Completed
Push — 0.8.dev ( 773e3d...108016 )
by Andrei
01:09
created

bang_visualizer.show_dendrogram()   B

Complexity

Conditions 3

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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