Completed
Push — 0.8.dev ( 6b2bc2...a929dc )
by Andrei
01:37
created

bang.__init__()   A

Complexity

Conditions 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
1
"""!
2
3
@brief Cluster analysis algorithm: BANG.
4
@details Implementation based on paper @cite inproceedings::bang::1.
5
6
@authors Andrei Novikov ([email protected])
7
@date 2014-2018
8
@copyright GNU Public License
9
10
@cond GNU_PUBLIC_LICENSE
11
    PyClustering is free software: you can redistribute it and/or modify
12
    it under the terms of the GNU General Public License as published by
13
    the Free Software Foundation, either version 3 of the License, or
14
    (at your option) any later version.
15
16
    PyClustering is distributed in the hope that it will be useful,
17
    but WITHOUT ANY WARRANTY; without even the implied warranty of
18
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
    GNU General Public License for more details.
20
21
    You should have received a copy of the GNU General Public License
22
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
@endcond
24
25
"""
26
27
28
import matplotlib
0 ignored issues
show
Configuration introduced by
The import matplotlib could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
29
import matplotlib.pyplot as plt
0 ignored issues
show
Configuration introduced by
The import matplotlib.pyplot could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
30
import matplotlib.patches as patches
0 ignored issues
show
Configuration introduced by
The import matplotlib.patches could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
31
32
from pyclustering.cluster import cluster_visualizer
33
from pyclustering.cluster.encoder import type_encoding
34
35
from pyclustering.utils import data_corners
36
from pyclustering.utils.color import color as color_list
37
38
39
40
class bang_visualizer:
41
    """!
42
    @brief Visualizer of BANG algorithm's results.
43
    @details BANG visualizer provides visualization services that are specific for BANG algorithm.
44
45
    """
46
47
48
    __maximum_density_alpha = 0.6
49
50
51
    @staticmethod
52
    def show_blocks(data, directory):
53
        """!
54
        @brief Show BANG-blocks (leafs only) in data space.
55
        @details BANG-blocks represents grid that was used for clustering process.
56
57
        @param[in] data (list): Input data space that contains points where each point is also represented by list.
58
        @param[in] directory (bang_directory): Directory that was created by BANG algorithm during clustering process.
59
60
        """
61
        visualizer = cluster_visualizer()
62
        visualizer.append_cluster(data)
63
64
        figure = visualizer.show(display=False)
65
66
        bang_visualizer.__draw_blocks(figure, 0, directory.get_leafs())
67
        plt.show()
68
69
70
    @staticmethod
71
    def show_dendrogram(dendrogram):
72
        """!
73
        @brief Display dendrogram of BANG-blocks.
74
75
        @param[in] dendrogram (list): List representation of dendrogram of BANG-blocks.
76
77
        @see bang.get_dendrogram()
78
79
        """
80
        axis = plt.subplot(111)
81
82
        current_position = 0
83
        for index_cluster in range(len(dendrogram)):
84
            densities = [ block.get_density() for block in dendrogram[index_cluster] ]
85
            xrange = range(current_position, current_position + len(densities))
86
87
            axis.bar(xrange, densities, 1.0, linewidth=0.0, color=color_list.get_color(index_cluster))
88
89
            current_position += len(densities)
90
91
        plt.xlim([-0.5, current_position - 0.5])
92
        plt.show()
93
94
95
    @staticmethod
96
    def __draw_blocks(figure, ax_index, blocks):
97
        """!
98
        @brief Display BANG-blocks on specified figure.
99
100
        @param[in] figure (figure): Figure that is used for drawing blocks.
101
        @param[in] ax_index (uint): Index of axis where blocks should be displayed.
102
        @param[in] blocks (list): List of blocks that should be displyed.
103
104
        """
105
        ax = figure.get_axes()[ax_index]
106
        ax.grid(False)
107
108
        density_scale = blocks[-1].get_density()
109
        for block in blocks:
110
            bang_visualizer.__draw_block(ax, block, density_scale)
111
112
113
    @staticmethod
114
    def __draw_block(ax, block, density_scale):
115
        """!
116
        @brief Display BANG-block on the specified ax.
117
118
        @param[in] ax (Axis): Axis where block should be displayed.
119
        @param[in] block (bang_block): BANG-block that should be displayed.
120
        @param[in] density_scale (double): Max density to display density of the block by appropriate tone.
121
122
        """
123
        max_corner, min_corner = block.get_spatial_block().get_corners()
124
        belong_cluster = block.get_cluster() is not None
125
126
        density_scale = bang_visualizer.__maximum_density_alpha * block.get_density() / density_scale
127
128
        face_color = matplotlib.colors.to_rgba('blue', alpha=density_scale)
129
        edge_color = matplotlib.colors.to_rgba('black', alpha=1.0)
130
131
        if len(max_corner) == 2:
132
            rect = patches.Rectangle(min_corner, max_corner[0] - min_corner[0], max_corner[1] - min_corner[1],
133
                                     fill=belong_cluster,
134
                                     facecolor=face_color,
135
                                     edgecolor=edge_color,
136
                                     linewidth=0.5)
137
            ax.add_patch(rect)
138
139
        else:
140
            raise ValueError("Impossible to display blocks on non-2D dimensional data.")
141
142
143
144
class bang_directory:
145
    """!
146
    @brief BANG directory stores BANG-blocks that represents grid in data space.
147
    @details The directory build BANG-blocks in binary tree manner. Leafs of the tree stored separately to provide
148
              a direct access to the leafs that should be analysed. Leafs cache data-points.
149
150
    """
151
    def __init__(self, data, levels, density_threshold=0.0):
152
        """!
153
        @brief Create BANG directory - basically tree structure with direct access to leafs.
154
155
        @param[in] levels (uint): Height of the blocks tree.
156
        @param[in] density_threshold (double): The lowest level of density when contained data by bang-block is
157
                    considered as a noise and there is no need to split it till the last level.
158
159
        """
160
        self.__data = data
161
        self.__levels = levels
162
        self.__density_threshold = density_threshold
163
        self.__leafs = []
164
        self.__root = None
165
166
        self.__create_directory()
167
168
169
    def get_leafs(self):
170
        """!
171
        @brief Return leafs - the smallest blocks.
172
        @details Some leafs can be bigger than others because splitting is not performed for blocks whose density is
173
                  less than threshold.
174
175
        @return (list) List of blocks that are leafs of BANG directory.
176
177
        """
178
        return self.__leafs
179
180
181
    def __create_directory(self):
182
        """!
183
        @brief Create BANG directory as a tree with separate storage for leafs.
184
185
        """
186
187
        min_corner, max_corner = data_corners(self.__data)
188
        data_block = spatial_block(max_corner, min_corner)
189
190
        cache_require = (self.__levels == 1)
191
        self.__root = bang_block(self.__data, 0, 0, data_block, cache_require)
192
193
        if cache_require:
194
            self.__leafs.append(self.__root)
195
        else:
196
            self.__build_directory_levels()
197
198
199
    def __build_directory_levels(self):
200
        """!
201
        @brief Build levels of direction if amount of level is greater than one.
202
203
        """
204
        previous_level_blocks = [ self.__root ]
205
        for level in range(1, self.__levels):
206
            previous_level_blocks = self.__build_level(previous_level_blocks, level)
207
208
        self.__leafs = sorted(self.__leafs, key=lambda block: block.get_density())
209
210
211
    def __build_level(self, previous_level_blocks, level):
212
        """!
213
        @brief Build new level of directory.
214
215
        @param[in] previous_level_blocks (list): BANG-blocks on the previous level.
216
        @param[in] level (uint): Level number that should be built.
217
218
        @return (list) New block on the specified level.
219
220
        """
221
        current_level_blocks = []
222
223
        split_dimension = level % len(self.__data[0])
224
        cache_require = (level == self.__levels - 1)
225
226
        for block in previous_level_blocks:
227
            self.__split_block(block, split_dimension, cache_require, current_level_blocks)
228
229
        if cache_require:
230
            self.__leafs += current_level_blocks
231
232
        return current_level_blocks
233
234
235
    def __split_block(self, block, split_dimension, cache_require, current_level_blocks):
236
        """!
237
        @brief Split specific block in specified dimension.
238
        @details Split is not performed for block whose density is lower than threshold value, such blocks are putted to
239
                  leafs.
240
241
        @param[in] block (bang_block): BANG-block that should be split.
242
        @param[in] split_dimension (uint): Dimension at which splitting should be performed.
243
        @param[in] cache_require (bool): Defines when points in cache should be stored during density calculation.
244
        @param[in|out] current_level_blocks (list): Block storage at the current level where new blocks should be added.
245
246
        """
247
        if block.get_density() <= self.__density_threshold:
248
            self.__leafs.append(block)
249
250
        else:
251
            left, right = block.split(split_dimension, cache_require)
252
            current_level_blocks.append(left)
253
            current_level_blocks.append(right)
254
255
256
257
class spatial_block:
258
    """!
259
    @brief Geometrical description of BANG block in data space.
260
    @details Provides services related to spatial function and used by bang_block
261
262
    @see bang_block
263
264
    """
265
266
    def __init__(self, max_corner, min_corner):
267
        """!
268
        @brief Creates spatial block in data space.
269
270
        @param[in] max_corner (array_like): Maximum corner coordinates of the block.
271
        @param[in] min_corner (array_like): Minimal corner coordinates of the block.
272
273
        """
274
        self.__max_corner = max_corner
275
        self.__min_corner = min_corner
276
        self.__volume = self.__calculate_volume()
277
278
279
    def __str__(self):
280
        """!
281
        @brief Returns string block description.
282
283
        @return String representation of the block.
284
285
        """
286
        return "(max: %s; min: %s)" % (self.__max_corner, self.__min_corner)
287
288
289
    def __contains__(self, point):
290
        """!
291
        @brief Point is considered as contained if it lies in block (belong to it).
292
293
        @return (bool) True if point is in block, otherwise False.
294
295
        """
296
        for i in range(len(point)):
297
            if point[i] < self.__min_corner[i] or point[i] > self.__max_corner[i]:
298
                return False
299
300
        return True
301
302
303
    def get_corners(self):
304
        """!
305
        @brief Return spatial description of current block.
306
307
        @return (tuple) Pair of maximum and minimum corners (max_corner, min_corner).
308
309
        """
310
        return self.__max_corner, self.__min_corner
311
312
313
    def get_volume(self):
314
        """!
315
        @brief Returns volume of current block.
316
        @details Volume block has uncommon mining here: for 1D is length of a line, for 2D is square of rectangle,
317
                  for 3D is volume of 3D figure, and for ND is volume of ND figure.
318
319
        @return (double) Volume of current block.
320
321
        """
322
        return self.__volume
323
324
325
    def split(self, dimension):
326
        """!
327
        @brief Split current block into two spatial blocks in specified dimension.
328
329
        @param[in] dimension (uint): Dimension where current block should be split.
330
331
        @return (tuple) Pair of new split blocks from current block.
332
333
        """
334
        first_max_corner = self.__max_corner[:]
335
        second_min_corner = self.__min_corner[:]
336
337
        split_border = (self.__max_corner[dimension] + self.__min_corner[dimension]) / 2.0
338
339
        first_max_corner[dimension] = split_border
340
        second_min_corner[dimension] = split_border
341
342
        return spatial_block(first_max_corner, self.__min_corner), spatial_block(self.__max_corner, second_min_corner)
343
344
345
    def is_neighbor(self, block):
346
        """!
347
        @brief Performs calculation to identify whether specified block is neighbor of current block.
348
349
        @param[in] block (spatial_block): Another block that is check whether it is neighbor.
350
351
        @return (bool) True is blocks are neighbors, False otherwise.
352
353
        """
354
        if block is not self:
355
            block_max_corner, _ = block.get_corners()
356
            dimension = len(block_max_corner)
357
            neighborhood_score = self.__calculate_neighborhood(block_max_corner)
358
359
            if neighborhood_score == dimension:
360
                return True
361
362
        return False
363
364
365
    def __calculate_neighborhood(self, block_max_corner):
366
        """!
367
        @brief Calculates neighborhood score that defined whether blocks are neighbors.
368
369
        @param[in] block_max_corner (list): Maximum coordinates of other block.
370
371
        @return (uint) Neighborhood score.
372
373
        """
374
        dimension = len(block_max_corner)
375
376
        length_edges = [self.__max_corner[i] - self.__min_corner[i] for i in range(dimension)]
377
378
        neighborhood_score = 0
379
        for i in range(dimension):
380
            diff = abs(block_max_corner[i] - self.__max_corner[i])
381
382
            if diff <= length_edges[i] + length_edges[i] * 0.0001:
383
                neighborhood_score += 1
384
385
        return neighborhood_score
386
387
388
    def __calculate_volume(self):
389
        """!
390
        @brief Calculates volume of current spatial block.
391
392
        @return (double) Volume of current spatial block.
393
394
        """
395
        volume = self.__max_corner[0] - self.__min_corner[0]
396
        for i in range(1, len(self.__max_corner)):
397
            volume *= self.__max_corner[i] - self.__min_corner[i]
398
399
        return volume
400
401
402
403
class bang_block:
404
    """!
405
    @brief BANG-block that represent spatial region in data space.
406
407
    """
408
    def __init__(self, data, region, level, space_block, cache_points=False):
409
        self.__data = data
410
        self.__region_number = region
411
        self.__level = level
412
        self.__spatial_block = space_block
413
        self.__cache_points = cache_points
414
415
        self.__cluster = None
416
        self.__points = None
417
        self.__density = self.__calculate_density()
418
419
420
    def __str__(self):
421
        """!
422
        @brief Returns string representation of BANG-block using region number and level where block is located.
423
424
        """
425
        return "(" + str(self.__region_number) + ", " + str(self.__level) + ")"
426
427
428
    def get_region(self):
429
        """!
430
        @brief Returns region number of BANG-block.
431
        @details Region number is unique on among region numbers on a directory level. Pair of region number and level
432
                  is unique for all directory.
433
434
        @return (uint) Region number.
435
436
        """
437
        return self.__region_number
438
439
440
    def get_density(self):
441
        """!
442
        @brief Returns density of the BANG-block.
443
444
        @return (double) BANG-block density.
445
446
        """
447
        return self.__density
448
449
450
    def get_cluster(self):
451
        """!
452
        @brief Return index of cluster to which the BANG-block belongs to.
453
        @details Index of cluster may have None value if the block was not assigned to any cluster.
454
455
        @return (uint) Index of cluster or None if the block does not belong to any cluster.
456
457
        """
458
        return self.__cluster
459
460
461
    def get_spatial_block(self):
462
        """!
463
        @brief Return spatial block - BANG-block description in data space.
464
465
        @return (spatial_block) Spatial block of the BANG-block.
466
467
        """
468
        return self.__spatial_block
469
470
471
    def get_points(self):
472
        """!
473
        @brief Return points that covers by the BANG-block.
474
        @details Returns None if block is not leaf.
475
476
        @return (list) List of point indexes that are covered by the block.
477
478
        """
479
        return self.__points
480
481
482
    def set_cluster(self, index):
483
        """!
484
        @brief Assign cluster to the BANG-block by index.
485
486
        @param[in] index (uint): Index cluster that is assigned to BANG-block.
487
488
        """
489
        self.__cluster = index
490
491
492
    def is_neighbor(self, block):
493
        """!
494
        @brief Performs calculation to check whether specified block is neighbor to the current.
495
496
        @param[in] block (bang_block): Other BANG-block that should be checked for neighborhood.
497
498
        @return (bool) True if blocks are neighbors, False if blocks are not neighbors.
499
500
        """
501
        return self.get_spatial_block().is_neighbor(block.get_spatial_block())
502
503
504
    def split(self, split_dimension, cache_points):
505
        """!
506
        @brief Split BANG-block into two new blocks in specified dimension.
507
508
        @param[in] split_dimension (uint): Dimension where block should be split.
509
        @param[in] cache_points (bool): If True then covered points are cached. Used for leaf blocks.
510
511
        @return (tuple) Pair of BANG-block that were formed from the current.
512
513
        """
514
        left_region_number = self.__region_number
515
        right_region_number = self.__region_number + 2 ** self.__level
516
517
        first_spatial_block, second_spatial_block = self.__spatial_block.split(split_dimension)
518
519
        left = bang_block(self.__data, left_region_number, self.__level + 1, first_spatial_block, cache_points)
520
        right = bang_block(self.__data, right_region_number, self.__level + 1, second_spatial_block, cache_points)
521
522
        return left, right
523
524
525
    def __calculate_density(self):
526
        """!
527
        @brief Calculates BANG-block density.
528
529
        @return (double) BANG-block density.
530
531
        """
532
        return self.__get_amount_points() / self.__spatial_block.get_volume()
533
534
535
    def __get_amount_points(self):
536
        """!
537
        @brief Count covered points by the BANG-block and if cache is enable then covered points are stored.
538
539
        @return (uint) Amount of covered points.
540
541
        """
542
        amount = 0
543
        for index in range(len(self.__data)):
544
            if self.__data[index] in self.__spatial_block:
545
                self.__cache_point(index)
546
                amount += 1
547
548
        return amount
549
550
551
    def __cache_point(self, index):
552
        """!
553
        @brief Store index points.
554
555
        @param[in] index (uint): Index point that should be stored.
556
557
        """
558
        if self.__cache_points:
559
            if self.__points is None:
560
                self.__points = []
561
562
            self.__points.append(index)
563
564
565
566
class bang:
567
    """!
568
    @brief Class implements BANG grid based clustering algorithm.
569
    @details BANG clustering algorithms uses a multidimensional grid structure to organize the value space surrounding
570
              the pattern values. The patterns are grouped into blocks and clustered with respect to the blocks by
571
              a topological neighbor search algorithm @cite inproceedings::bang::1.
572
573
    """
574
575
    def __init__(self, data, levels, density_threshold=0.0, ccore=False):
0 ignored issues
show
Unused Code introduced by
The argument ccore seems to be unused.
Loading history...
576
        """!
577
        @brief Create BANG clustering algorithm.
578
579
        @param[in] data (list): Input data (list of points) that should be clustered.
580
        @param[in] levels (uint): Amount of levels in tree (how many times block should be split).
581
        @param[in] density_threshold (double): If block density is smaller than this value then contained data by this
582
                    block is considered as a noise.
583
        @param[in] ccore (bool): Reserved positional argument - not used yet.
584
585
        """
586
        self.__data = data
587
        self.__levels = levels
588
        self.__directory = None
589
        self.__clusters = []
590
        self.__noise = []
591
        self.__cluster_blocks = []
592
        self.__dendrogram = [[]]
593
        self.__density_threshold = density_threshold
594
595
596
    def process(self):
597
        """!
598
        @brief Performs clustering process in line with rules of BANG clustering algorithm.
599
600
        @see get_clusters()
601
        @see get_noise()
602
        @see get_directory()
603
        @see get_dendrogram()
604
605
        """
606
        self.__validate_arguments()
607
608
        self.__directory = bang_directory(self.__data, self.__levels, self.__density_threshold)
609
        self.__allocate_clusters()
610
611
612
    def get_clusters(self):
613
        """!
614
        @brief Returns allocated clusters.
615
616
        @remark Allocated clusters are returned only after data processing (method process()). Otherwise empty list is returned.
617
618
        @return (list) List of allocated clusters, each cluster contains indexes of objects in list of data.
619
620
        @see process()
621
        @see get_noise()
622
623
        """
624
        return self.__clusters
625
626
627
    def get_noise(self):
628
        """!
629
        @brief Returns allocated noise.
630
631
        @remark Allocated noise is returned only after data processing (method process()). Otherwise empty list is returned.
632
633
        @return (list) List of indexes that are marked as a noise.
634
635
        @see process()
636
        @see get_clusters()
637
638
        """
639
        return self.__noise
640
641
642
    def get_directory(self):
643
        """!
644
        @brief Returns grid directory that describes grid of the processed data.
645
646
        @remark Grid directory is returned only after data processing (method process()). Otherwise None value is returned.
647
648
        @return (bang_directory) BANG directory that describes grid of process data.
649
650
        @see process()
651
652
        """
653
        return self.__directory
654
655
656
    def get_dendrogram(self):
657
        """!
658
        @brief Returns dendrogram of clusters.
659
        @details Dendrogram is created in following way: the density indices of all regions are calculated and sorted
660
                  in decreasing order for each cluster during clustering process.
661
662
        @remark Dendrogram is returned only after data processing (method process()). Otherwise empty list is returned.
663
664
        """
665
        return self.__dendrogram
666
667
668
    def get_cluster_encoding(self):
669
        """!
670
        @brief Returns clustering result representation type that indicate how clusters are encoded.
671
672
        @return (type_encoding) Clustering result representation.
673
674
        @see get_clusters()
675
676
        """
677
678
        return type_encoding.CLUSTER_INDEX_LIST_SEPARATION
679
680
681
    def __validate_arguments(self):
682
        """!
683
        @brief Check input arguments of BANG algorithm and if one of them is not correct then appropriate exception
684
                is thrown.
685
686
        """
687
        if self.__levels <= 0:
688
            raise ValueError("Incorrect amount of levels '%d'. Level value should be greater than 0." % self.__levels)
689
690
        if len(self.__data) == 0:
691
            raise ValueError("Empty input data. Data should contain at least one point.")
692
693
        if self.__density_threshold < 0:
694
            raise ValueError("Incorrect density threshold '%f'. Density threshold should not be negative." % self.__density_threshold)
695
696
697
    def __allocate_clusters(self):
698
        """!
699
        @brief Performs cluster allocation using leafs of tree in BANG directory (the smallest cells).
700
701
        """
702
        leaf_blocks = self.__directory.get_leafs()
703
        unhandled_block_indexes = set([i for i in range(len(leaf_blocks)) if leaf_blocks[i].get_density() > self.__density_threshold])
704
        appropriate_block_indexes = set(unhandled_block_indexes)
705
706
        current_block = self.__find_block_center(leaf_blocks, unhandled_block_indexes)
707
        cluster_index = 0
708
709
        while current_block is not None:
710
            if current_block.get_density() <= self.__density_threshold:
711
                break
712
713
            self.__expand_cluster_block(current_block, cluster_index, leaf_blocks, unhandled_block_indexes)
714
715
            current_block = self.__find_block_center(leaf_blocks, unhandled_block_indexes)
716
            cluster_index += 1
717
718
        self.__store_clustering_results(cluster_index, appropriate_block_indexes, leaf_blocks)
719
720
721
    def __expand_cluster_block(self, block, cluster_index, leaf_blocks, unhandled_block_indexes):
722
        """!
723
        @brief Expand cluster from specific block that is considered as a central block.
724
725
        @param[in] block (bang_block): Block that is considered as a central block for cluster.
726
        @param[in] cluster_index (uint): Index of cluster that is assigned to blocks that forms new cluster.
727
        @param[in] leaf_blocks (list): Leaf BANG-blocks that are considered during cluster formation.
728
        @param[in] unhandled_block_indexes (set): Set of candidates (BANG block indexes) to become a cluster member. The
729
                    parameter helps to reduce traversing among BANG-block providing only restricted set of block that
730
                    should be considered.
731
732
        """
733
734
        block.set_cluster(cluster_index)
735
        self.__update_cluster_dendrogram(cluster_index, [block])
736
737
        neighbors = self.__find_block_neighbors(block, leaf_blocks, unhandled_block_indexes)
738
        self.__update_cluster_dendrogram(cluster_index, neighbors)
739
740
        for neighbor in neighbors:
741
            neighbor.set_cluster(cluster_index)
742
            neighbor_neighbors = self.__find_block_neighbors(neighbor, leaf_blocks, unhandled_block_indexes)
743
            self.__update_cluster_dendrogram(cluster_index, neighbor_neighbors)
744
745
            neighbors += neighbor_neighbors
746
747
748
    def __store_clustering_results(self, amount_clusters, appropriate_block_indexes, leaf_blocks):
749
        """!
750
        @brief Stores clustering results in a convenient way.
751
752
        @param[in] amount_clusters (uint): Amount of cluster that was allocated during processing.
753
        @param[in] appropriate_block_indexes (list): BANG-block their indexes that forms clusters.
754
        @param[in] leaf_blocks (list): Leaf BANG-blocks (the smallest cells).
755
756
        """
757
        self.__clusters = [[] for _ in range(amount_clusters)]
758
        for appropriate_index in appropriate_block_indexes:
759
            block = leaf_blocks[appropriate_index]
760
            index = block.get_cluster()
761
762
            if index is not None:
763
                self.__clusters[index] += block.get_points()
764
            else:
765
                self.__noise += block.get_points()
766
767
        self.__clusters = [ list(set(cluster)) for cluster in self.__clusters ]
768
        self.__noise = list(set(self.__noise))
769
770
771
    def __find_block_center(self, level_blocks, unhandled_block_indexes):
772
        """!
773
        @brief Search block that is cluster center for new cluster.
774
775
        @return (bang_block) Central block for new cluster, if cluster is not found then None value is returned.
776
777
        """
778
        for i in reversed(range(len(level_blocks))):
779
            if level_blocks[i].get_density() == 0:
780
                return None
781
782
            if level_blocks[i].get_cluster() is None:
783
                unhandled_block_indexes.remove(i)
784
                return level_blocks[i]
785
786
        return None
787
788
789
    def __find_block_neighbors(self, block, level_blocks, unhandled_block_indexes):
790
        """!
791
        @brief Search block neighbors that are parts of new clusters (density is greater than threshold and that are
792
                not cluster members yet), other neighbors are ignored.
793
794
        @param[in] block (bang_block): BANG-block for which neighbors should be found (which can be part of cluster).
795
        @param[in] level_blocks (list): BANG-blocks on specific level.
796
        @param[in] unhandled_block_indexes (set): Blocks that have not been processed yet.
797
798
        @return (list) Block neighbors that can become part of cluster.
799
800
        """
801
        neighbors = []
802
803
        handled_block_indexes = []
804
        for unhandled_index in unhandled_block_indexes:
805
            if block.is_neighbor(level_blocks[unhandled_index]):
806
                handled_block_indexes.append(unhandled_index)
807
                neighbors.append(level_blocks[unhandled_index])
808
809
                # Maximum number of neighbors is eight
810
                if len(neighbors) == 8:
811
                    break
812
813
        for handled_index in handled_block_indexes:
814
            unhandled_block_indexes.remove(handled_index)
815
816
        return neighbors
817
818
819
    def __update_cluster_dendrogram(self, index_cluster, blocks):
820
        """!
821
        @brief Append clustered blocks to dendrogram.
822
823
        @param[in] index_cluster (uint): Cluster index that was assigned to blocks.
824
        @param[in] blocks (list): Blocks that were clustered.
825
826
        """
827
        if len(self.__dendrogram) <= index_cluster:
828
            self.__dendrogram.append([])
829
830
        blocks = sorted(blocks, key=lambda block: block.get_density(), reverse=True)
831
        self.__dendrogram[index_cluster] += blocks
832