File: sync.py

package info (click to toggle)
python-pyclustering 0.10.1.2-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 11,128 kB
  • sloc: cpp: 38,888; python: 24,311; sh: 384; makefile: 105
file content (1044 lines) | stat: -rwxr-xr-x 45,315 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
"""!

@brief Neural Network: Oscillatory Neural Network based on Kuramoto model
@details Implementation based on paper @cite article::syncnet::1, @cite article::nnet::sync::1, @cite inproceedings::net::sync::1.

@authors Andrei Novikov (pyclustering@yandex.ru)
@date 2014-2020
@copyright BSD-3-Clause

"""

import math
import numpy
import random

import matplotlib.pyplot as plt
import matplotlib.animation as animation

import pyclustering.core.sync_wrapper as wrapper

from pyclustering.core.wrapper import ccore_library

from scipy.integrate import odeint

from pyclustering.nnet import network, conn_represent, conn_type, initial_type, solve_type
from pyclustering.utils import pi, draw_dynamics, draw_dynamics_set, set_ax_param


class order_estimator:
    """!
    @brief Provides services to calculate order parameter and local order parameter that are used
            for synchronization level estimation.

    """
    @staticmethod
    def calculate_sync_order(oscillator_phases):
        """!
        @brief Calculates level of global synchronization (order parameter) for input phases.
        @details This parameter is tend 1.0 when the oscillatory network close to global synchronization and it tend to 0.0 when 
                  desynchronization is observed in the network.
        
        @param[in] oscillator_phases (list): List of oscillator phases that are used for level of global synchronization.
        
        @return (double) Level of global synchronization (order parameter).
        
        @see calculate_order_parameter()
        
        """

        exp_amount = 0.0
        average_phase = 0.0

        for phase in oscillator_phases:
            exp_amount += math.expm1(abs(1j * phase))
            average_phase += phase

        exp_amount /= len(oscillator_phases)
        average_phase = math.expm1(abs(1j * (average_phase / len(oscillator_phases))))

        return abs(average_phase) / abs(exp_amount)


    @staticmethod
    def calculate_local_sync_order(oscillator_phases, oscillatory_network):
        """!
        @brief Calculates level of local synchorization (local order parameter) for input phases for the specified network.
        @details This parameter is tend 1.0 when the oscillatory network close to local synchronization and it tend to 0.0 when 
                  desynchronization is observed in the network.
        
        @param[in] oscillator_phases (list): List of oscillator phases that are used for level of local (partial) synchronization.
        @param[in] oscillatory_network (sync): Instance of oscillatory network whose connections are required for calculation.
        
        @return (double) Level of local synchronization (local order parameter).
        
        """

        exp_amount = 0.0
        num_neigh = 0.0

        for i in range(0, len(oscillatory_network), 1):
            for j in range(0, len(oscillatory_network), 1):
                if oscillatory_network.has_connection(i, j) is True:
                    exp_amount += math.exp(-abs(oscillator_phases[j] - oscillator_phases[i]))
                    num_neigh += 1.0

        if num_neigh == 0:
            num_neigh = 1.0

        return exp_amount / num_neigh



class sync_dynamic:
    """!
    @brief Represents output dynamic of Sync.
    
    """

    @property
    def output(self):
        """!
        @brief (list) Returns output dynamic of the Sync network (phase coordinates of each oscillator in the network) during simulation.
        
        """
        if (self._ccore_sync_dynamic_pointer is not None) and ((self._dynamic is None) or (len(self._dynamic) == 0)):
            self._dynamic = wrapper.sync_dynamic_get_output(self._ccore_sync_dynamic_pointer)

        return self._dynamic


    @property
    def time(self):
        """!
        @brief (list) Returns sampling times when dynamic is measured during simulation.
        
        """
        if (self._ccore_sync_dynamic_pointer is not None) and ((self._time is None) or (len(self._time) == 0)):
            self._time = wrapper.sync_dynamic_get_time(self._ccore_sync_dynamic_pointer)

        return self._time


    def __init__(self, phase, time, ccore=None):
        """!
        @brief Constructor of Sync dynamic.
        
        @param[in] phase (list): Dynamic of oscillators on each step of simulation. If ccore pointer is specified than it can be ignored.
        @param[in] time (list): Simulation time.
        @param[in] ccore (ctypes.pointer): Pointer to CCORE sync_dynamic instance in memory.
        
        """

        self._dynamic = phase
        self._time = time
        self._ccore_sync_dynamic_pointer = ccore


    def __del__(self):
        """!
        @brief Default destructor of Sync dynamic.
        
        """
        if self._ccore_sync_dynamic_pointer is not None:
            wrapper.sync_dynamic_destroy(self._ccore_sync_dynamic_pointer)


    def __len__(self):
        """!
        @brief Returns number of simulation steps that are stored in dynamic.
        @return (uint) Number of simulation steps that are stored in dynamic.
        
        """
        if (self._ccore_sync_dynamic_pointer is not None):
            return wrapper.sync_dynamic_get_size(self._ccore_sync_dynamic_pointer)

        return len(self._dynamic)


    def __getitem__(self, index):
        """!
        @brief Indexing of the dynamic.
        
        """
        if index == 0:
            return self.time

        elif index == 1:
            return self.output

        else:
            raise NameError('Out of range ' + index + ': only indexes 0 and 1 are supported.')


    def allocate_sync_ensembles(self, tolerance = 0.01, indexes = None, iteration = None):
        """!
        @brief Allocate clusters in line with ensembles of synchronous oscillators where each synchronous ensemble corresponds to only one cluster.
               
        @param[in] tolerance (double): Maximum error for allocation of synchronous ensemble oscillators.
        @param[in] indexes (list): List of real object indexes and it should be equal to amount of oscillators (in case of 'None' - indexes are in range [0; amount_oscillators]).
        @param[in] iteration (uint): Iteration of simulation that should be used for allocation.
        
        @return (list) Groups (lists) of indexes of synchronous oscillators.
                For example [ [index_osc1, index_osc3], [index_osc2], [index_osc4, index_osc5] ].
        
        """

        if self._ccore_sync_dynamic_pointer is not None:
            ensembles = wrapper.sync_dynamic_allocate_sync_ensembles(self._ccore_sync_dynamic_pointer, tolerance, iteration)

            if indexes is not None:
                for ensemble in ensembles:
                    for index in range(len(ensemble)):
                        ensemble[index] = indexes[ensemble[index]]

            return ensembles

        if (self._dynamic is None) or (len(self._dynamic) == 0):
            return []

        number_oscillators = len(self._dynamic[0])
        last_state = None

        if iteration is None:
            last_state = self._dynamic[len(self._dynamic) - 1]
        else:
            last_state = self._dynamic[iteration]

        clusters = []
        if number_oscillators > 0:
            clusters.append([0])

        for i in range(1, number_oscillators, 1):
            cluster_allocated = False
            for cluster in clusters:
                for neuron_index in cluster:
                    last_state_shifted = abs(last_state[i] - 2 * pi)

                    if ( ( (last_state[i] < (last_state[neuron_index] + tolerance)) and (last_state[i] > (last_state[neuron_index] - tolerance)) ) or
                         ( (last_state_shifted < (last_state[neuron_index] + tolerance)) and (last_state_shifted > (last_state[neuron_index] - tolerance)) ) ):
                        cluster_allocated = True

                        real_index = i
                        if indexes is not None:
                            real_index = indexes[i]

                        cluster.append(real_index)
                        break

                if cluster_allocated is True:
                    break

            if cluster_allocated is False:
                clusters.append([i])

        return clusters


    def allocate_phase_matrix(self, grid_width = None, grid_height = None, iteration = None):
        """!
        @brief Returns 2D matrix of phase values of oscillators at the specified iteration of simulation.
        @details User should ensure correct matrix sizes in line with following expression grid_width x grid_height that should be equal to 
                  amount of oscillators otherwise exception is thrown. If grid_width or grid_height are not specified than phase matrix size 
                  will by calculated automatically by square root.
        
        @param[in] grid_width (uint): Width of the allocated matrix.
        @param[in] grid_height (uint): Height of the allocated matrix.
        @param[in] iteration (uint): Number of iteration of simulation for which correlation matrix should be allocated.
                    If iternation number is not specified, the last step of simulation is used for the matrix allocation.
        
        @return (list) Phase value matrix of oscillators with size [number_oscillators x number_oscillators].
        
        """

        output_dynamic = self.output

        if (output_dynamic is None) or (len(output_dynamic) == 0):
            return []

        current_dynamic = output_dynamic[len(output_dynamic) - 1]
        if iteration is not None:
            current_dynamic = output_dynamic[iteration]

        width_matrix = grid_width
        height_matrix = grid_height
        number_oscillators = len(current_dynamic)
        if (width_matrix is None) or (height_matrix is None):
            width_matrix = int(math.ceil(math.sqrt(number_oscillators)))
            height_matrix = width_matrix

        if (number_oscillators != width_matrix * height_matrix):
            raise NameError("Impossible to allocate phase matrix with specified sizes, amout of neurons should be equal to grid_width * grid_height.");

        phase_matrix = [[0.0 for _ in range(width_matrix)] for _ in range(height_matrix)]
        for i in range(height_matrix):
            for j in range(width_matrix):
                phase_matrix[i][j] = current_dynamic[j + i * width_matrix]

        return phase_matrix


    def allocate_correlation_matrix(self, iteration = None):
        """!
        @brief Allocate correlation matrix between oscillators at the specified step of simulation.
               
        @param[in] iteration (uint): Number of iteration of simulation for which correlation matrix should be allocated.
                    If iternation number is not specified, the last step of simulation is used for the matrix allocation.
        
        @return (list) Correlation matrix between oscillators with size [number_oscillators x number_oscillators].
        
        """

        if self._ccore_sync_dynamic_pointer is not None:
            return wrapper.sync_dynamic_allocate_correlation_matrix(self._ccore_sync_dynamic_pointer, iteration)

        if (self._dynamic is None) or (len(self._dynamic) == 0):
            return []

        dynamic = self._dynamic
        current_dynamic = dynamic[len(dynamic) - 1]

        if iteration is not None:
            current_dynamic = dynamic[iteration]

        number_oscillators = len(dynamic[0])
        affinity_matrix = [[0.0 for i in range(number_oscillators)] for j in range(number_oscillators)]

        for i in range(number_oscillators):
            for j in range(number_oscillators):
                phase1 = current_dynamic[i]
                phase2 = current_dynamic[j]

                affinity_matrix[i][j] = abs(math.sin(phase1 - phase2))

        return affinity_matrix


    def calculate_order_parameter(self, start_iteration=None, stop_iteration=None):
        """!
        @brief Calculates level of global synchorization (order parameter).
        @details This parameter is tend 1.0 when the oscillatory network close to global synchronization and it tend to 0.0 when 
                  desynchronization is observed in the network. Order parameter is calculated using following equation:
                  
                  \f[
                  r_{c}=\frac{1}{Ne^{i\varphi }}\sum_{j=0}^{N}e^{i\theta_{j}};
                  \f]
                  
                  where \f$\varphi\f$ is a average phase coordinate in the network, \f$N\f$ is an amount of oscillators in the network.
        
        @param[in] start_iteration (uint): The first iteration that is used for calculation, if 'None' then the last iteration is used.
        @param[in] stop_iteration (uint): The last iteration that is used for calculation, if 'None' then 'start_iteration' + 1 is used.
        
        Example:
        @code
            oscillatory_network = sync(16, type_conn = conn_type.ALL_TO_ALL);
            output_dynamic = oscillatory_network.simulate_static(100, 10);
            
            print("Order parameter at the last step: ", output_dynamic.calculate_order_parameter());
            print("Order parameter at the first step:", output_dynamic.calculate_order_parameter(0));
            print("Order parameter evolution between 40 and 50 steps:", output_dynamic.calculate_order_parameter(40, 50));
        @endcode
        
        @return (list) List of levels of global synchronization (order parameter evolution).
        
        @see order_estimator
        
        """

        (start_iteration, stop_iteration) = self.__get_start_stop_iterations(start_iteration, stop_iteration)

        if self._ccore_sync_dynamic_pointer is not None:
            return wrapper.sync_dynamic_calculate_order(self._ccore_sync_dynamic_pointer, start_iteration, stop_iteration)

        sequence_order = []
        for index in range(start_iteration, stop_iteration):
            sequence_order.append(order_estimator.calculate_sync_order(self.output[index]))

        return sequence_order


    def calculate_local_order_parameter(self, oscillatory_network, start_iteration = None, stop_iteration = None):
        """!
        @brief Calculates local order parameter.
        @details Local order parameter or so-called level of local or partial synchronization is calculated by following expression:
        
        \f[
        r_{c}=\left | \sum_{i=0}^{N} \frac{1}{N_{i}} \sum_{j=0}e^{ \theta_{j} - \theta_{i} } \right |;
        \f]
        
        where N - total amount of oscillators in the network and \f$N_{i}\f$ - amount of neighbors of oscillator with index \f$i\f$.
        
        @param[in] oscillatory_network (sync): Sync oscillatory network whose structure of connections is required for calculation.
        @param[in] start_iteration (uint): The first iteration that is used for calculation, if 'None' then the last iteration is used.
        @param[in] stop_iteration (uint): The last iteration that is used for calculation, if 'None' then 'start_iteration' + 1 is used.
        
        @return (list) List of levels of local (partial) synchronization (local order parameter evolution).
        
        """

        (start_iteration, stop_iteration) = self.__get_start_stop_iterations(start_iteration, stop_iteration)

        if self._ccore_sync_dynamic_pointer is not None:
            network_pointer = oscillatory_network._ccore_network_pointer
            return wrapper.sync_dynamic_calculate_local_order(self._ccore_sync_dynamic_pointer, network_pointer, start_iteration, stop_iteration)

        sequence_local_order = []
        for index in range(start_iteration, stop_iteration):
            sequence_local_order.append(order_estimator.calculate_local_sync_order(self.output[index], oscillatory_network))

        return sequence_local_order


    def __get_start_stop_iterations(self, start_iteration, stop_iteration):
        """!
        @brief Aplly rules for start_iteration and stop_iteration parameters.

        @param[in] start_iteration (uint): The first iteration that is used for calculation.
        @param[in] stop_iteration (uint): The last iteration that is used for calculation.
        
        @return (tuple) New the first iteration and the last.
        
        """
        if start_iteration is None:
            start_iteration = len(self) - 1

        if stop_iteration is None:
            stop_iteration = start_iteration + 1

        return start_iteration, stop_iteration



class sync_visualizer:
    """!
    @brief Visualizer of output dynamic of sync network (Sync).
    
    """

    @staticmethod
    def show_output_dynamic(sync_output_dynamic):
        """!
        @brief Shows output dynamic (output of each oscillator) during simulation.
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network.
        
        @see show_output_dynamics
        
        """

        draw_dynamics(sync_output_dynamic.time, sync_output_dynamic.output, x_title="t", y_title="phase", y_lim=[0, 2 * 3.14])


    @staticmethod
    def show_output_dynamics(sync_output_dynamics):
        """!
        @brief Shows several output dynamics (output of each oscillator) during simulation.
        @details Each dynamic is presented on separate plot.
        
        @param[in] sync_output_dynamics (list): list of output dynamics 'sync_dynamic' of the Sync network.
        
        @see show_output_dynamic
        
        """

        draw_dynamics_set(sync_output_dynamics, "t", "phase", None, [0, 2 * 3.14], False, False)


    @staticmethod
    def show_correlation_matrix(sync_output_dynamic, iteration=None):
        """!
        @brief Shows correlation matrix between oscillators at the specified iteration.
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network.
        @param[in] iteration (uint): Number of iteration of simulation for which correlation matrix should be
                    allocated. If iteration number is not specified, the last step of simulation is used for the matrix
                    allocation.
        
        """

        _ = plt.figure()
        correlation_matrix = sync_output_dynamic.allocate_correlation_matrix(iteration)

        plt.imshow(correlation_matrix, cmap = plt.get_cmap('cool'), interpolation='kaiser', vmin=0.0, vmax=1.0)
        plt.show()


    @staticmethod
    def show_phase_matrix(sync_output_dynamic, grid_width=None, grid_height=None, iteration=None):
        """!
        @brief Shows 2D matrix of phase values of oscillators at the specified iteration.
        @details User should ensure correct matrix sizes in line with following expression grid_width x grid_height that should be equal to 
                  amount of oscillators otherwise exception is thrown. If grid_width or grid_height are not specified than phase matrix size 
                  will by calculated automatically by square root.
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network whose phase matrix should be shown.
        @param[in] grid_width (uint): Width of the phase matrix.
        @param[in] grid_height (uint): Height of the phase matrix.
        @param[in] iteration (uint): Number of iteration of simulation for which correlation matrix should be allocated.
                    If iternation number is not specified, the last step of simulation is used for the matrix allocation.
        
        """

        _ = plt.figure()
        phase_matrix = sync_output_dynamic.allocate_phase_matrix(grid_width, grid_height, iteration)

        plt.imshow(phase_matrix, cmap = plt.get_cmap('jet'), interpolation='kaiser', vmin=0.0, vmax=2.0 * math.pi)
        plt.show()


    @staticmethod
    def show_order_parameter(sync_output_dynamic, start_iteration=None, stop_iteration=None):
        """!
        @brief Shows evolution of order parameter (level of global synchronization in the network).
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network whose evolution of global synchronization should be visualized.
        @param[in] start_iteration (uint): The first iteration that is used for calculation, if 'None' then the first is used
        @param[in] stop_iteration (uint): The last iteration that is used for calculation, if 'None' then the last is used.
        
        """

        (start_iteration, stop_iteration) = sync_visualizer.__get_start_stop_iterations(sync_output_dynamic, start_iteration, stop_iteration)

        order_parameter = sync_output_dynamic.calculate_order_parameter(start_iteration, stop_iteration)
        axis = plt.subplot(111)
        plt.plot(sync_output_dynamic.time[start_iteration:stop_iteration], order_parameter, 'b-', linewidth=2.0)
        set_ax_param(axis, "t", "R (order parameter)", None, [0.0, 1.05])

        plt.show()


    @staticmethod
    def show_local_order_parameter(sync_output_dynamic, oscillatory_network, start_iteration=None, stop_iteration=None):
        """!
        @brief Shows evolution of local order parameter (level of local synchronization in the network).
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network whose evolution of global synchronization should be visualized.
        @param[in] oscillatory_network (sync): Sync oscillatory network whose structure of connections is required for calculation.
        @param[in] start_iteration (uint): The first iteration that is used for calculation, if 'None' then the first is used
        @param[in] stop_iteration (uint): The last iteration that is used for calculation, if 'None' then the last is used.
        
        """
        (start_iteration, stop_iteration) = sync_visualizer.__get_start_stop_iterations(sync_output_dynamic, start_iteration, stop_iteration)

        order_parameter = sync_output_dynamic.calculate_local_order_parameter(oscillatory_network, start_iteration, stop_iteration)
        axis = plt.subplot(111)
        plt.plot(sync_output_dynamic.time[start_iteration:stop_iteration], order_parameter, 'b-', linewidth=2.0)
        set_ax_param(axis, "t", "R (local order parameter)", None, [0.0, 1.05])

        plt.show()


    @staticmethod
    def animate_output_dynamic(sync_output_dynamic, animation_velocity = 75, save_movie = None):
        """!
        @brief Shows animation of output dynamic (output of each oscillator) during simulation on a circle from [0; 2pi].
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network.
        @param[in] animation_velocity (uint): Interval between frames in milliseconds.
        @param[in] save_movie (string): If it is specified then animation will be stored to file that is specified in this parameter.
        
        """

        figure = plt.figure()

        dynamic = sync_output_dynamic.output[0]
        artist, = plt.polar(dynamic, [1.0] * len(dynamic), 'o', color='blue')

        def init_frame():
            return [artist]

        def frame_generation(index_dynamic):
            dynamic = sync_output_dynamic.output[index_dynamic]
            artist.set_data(dynamic, [1.0] * len(dynamic))

            return [artist]

        phase_animation = animation.FuncAnimation(figure, frame_generation, len(sync_output_dynamic), interval = animation_velocity, init_func = init_frame, repeat_delay = 5000);

        if save_movie is not None:
            phase_animation.save(save_movie, writer='ffmpeg', fps=15, bitrate=1500)
        else:
            plt.show()


    @staticmethod
    def animate_correlation_matrix(sync_output_dynamic, animation_velocity = 75, colormap = 'cool', save_movie = None):
        """!
        @brief Shows animation of correlation matrix between oscillators during simulation.
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network.
        @param[in] animation_velocity (uint): Interval between frames in milliseconds.
        @param[in] colormap (string): Name of colormap that is used by matplotlib ('gray', 'pink', 'cool', spring', etc.).
        @param[in] save_movie (string): If it is specified then animation will be stored to file that is specified in this parameter.
        
        """

        figure = plt.figure()

        correlation_matrix = sync_output_dynamic.allocate_correlation_matrix(0)
        artist = plt.imshow(correlation_matrix, cmap = plt.get_cmap(colormap), interpolation='kaiser', vmin = 0.0, vmax = 1.0)

        def init_frame():
            return [ artist ]

        def frame_generation(index_dynamic):
            correlation_matrix = sync_output_dynamic.allocate_correlation_matrix(index_dynamic)
            artist.set_data(correlation_matrix)

            return [artist]

        correlation_animation = animation.FuncAnimation(figure, frame_generation, len(sync_output_dynamic), init_func = init_frame, interval = animation_velocity , repeat_delay = 1000, blit = True)

        if save_movie is not None:
            correlation_animation.save(save_movie, writer='ffmpeg', fps=15, bitrate=1500)
        else:
            plt.show()


    @staticmethod
    def animate_phase_matrix(sync_output_dynamic, grid_width=None, grid_height=None, animation_velocity=75, colormap='jet', save_movie=None):
        """!
        @brief Shows animation of phase matrix between oscillators during simulation on 2D stage.
        @details If grid_width or grid_height are not specified than phase matrix size will by calculated automatically by square root.
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network.
        @param[in] grid_width (uint): Width of the phase matrix.
        @param[in] grid_height (uint): Height of the phase matrix.
        @param[in] animation_velocity (uint): Interval between frames in milliseconds.
        @param[in] colormap (string): Name of colormap that is used by matplotlib ('gray', 'pink', 'cool', spring', etc.).
        @param[in] save_movie (string): If it is specified then animation will be stored to file that is specified in this parameter.
        
        """

        figure = plt.figure()

        def init_frame():
            return frame_generation(0)

        def frame_generation(index_dynamic):
            figure.clf()
            axis = figure.add_subplot(111)

            phase_matrix = sync_output_dynamic.allocate_phase_matrix(grid_width, grid_height, index_dynamic)
            axis.imshow(phase_matrix, cmap=plt.get_cmap(colormap), interpolation='kaiser', vmin=0.0, vmax=2.0 * math.pi)
            artist = figure.gca()

            return [artist]

        phase_animation = animation.FuncAnimation(figure, frame_generation, len(sync_output_dynamic), init_func = init_frame, interval = animation_velocity , repeat_delay = 1000);

        if save_movie is not None:
            phase_animation.save(save_movie, writer='ffmpeg', fps=15, bitrate=1500)
        else:
            plt.show()


    @staticmethod
    def __get_start_stop_iterations(sync_output_dynamic, start_iteration, stop_iteration):
        """!
        @brief Apply rule of preparation for start iteration and stop iteration values.
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network.
        @param[in] start_iteration (uint): The first iteration that is used for calculation.
        @param[in] stop_iteration (uint): The last iteration that is used for calculation.
        
        @return (tuple) New values of start and stop iterations.
        
        """
        if start_iteration is None:
            start_iteration = 0

        if stop_iteration is None:
            stop_iteration = len(sync_output_dynamic)

        return start_iteration, stop_iteration


    @staticmethod
    def animate(sync_output_dynamic, title=None, save_movie=None):
        """!
        @brief Shows animation of phase coordinates and animation of correlation matrix together for the Sync dynamic output on the same figure.
        
        @param[in] sync_output_dynamic (sync_dynamic): Output dynamic of the Sync network.
        @param[in] title (string): Title of the animation that is displayed on a figure if it is specified.
        @param[in] save_movie (string): If it is specified then animation will be stored to file that is specified in this parameter.
        
        """

        dynamic = sync_output_dynamic.output[0]
        correlation_matrix = sync_output_dynamic.allocate_correlation_matrix(0)

        figure = plt.figure(1)
        if title is not None:
            figure.suptitle(title, fontsize = 26, fontweight = 'bold')

        ax1 = figure.add_subplot(121, projection='polar')
        ax2 = figure.add_subplot(122)

        artist1, = ax1.plot(dynamic, [1.0] * len(dynamic), marker='o', color='blue', ls='')
        artist2 = ax2.imshow(correlation_matrix, cmap = plt.get_cmap('Accent'), interpolation='kaiser')

        def init_frame():
            return [artist1, artist2]

        def frame_generation(index_dynamic):
            dynamic = sync_output_dynamic.output[index_dynamic]
            artist1.set_data(dynamic, [1.0] * len(dynamic))

            correlation_matrix = sync_output_dynamic.allocate_correlation_matrix(index_dynamic)
            artist2.set_data(correlation_matrix)

            return [artist1, artist2]

        dynamic_animation = animation.FuncAnimation(figure, frame_generation, len(sync_output_dynamic), interval=75, init_func=init_frame, repeat_delay=5000)

        if save_movie is not None:
            dynamic_animation.save(save_movie, writer='ffmpeg', fps=15, bitrate=1500)
        else:
            plt.show()



class sync_network(network):
    """!
    @brief Model of oscillatory network that is based on the Kuramoto model of synchronization.
    
    @details CCORE option can be used to use the pyclustering core - C/C++ shared library for processing that significantly increases performance.
    
    """

    def __init__(self, num_osc, weight = 1, frequency = 0, type_conn = conn_type.ALL_TO_ALL, representation = conn_represent.MATRIX, initial_phases = initial_type.RANDOM_GAUSSIAN, ccore = True):
        """!
        @brief Constructor of oscillatory network is based on Kuramoto model.
        
        @param[in] num_osc (uint): Number of oscillators in the network.
        @param[in] weight (double): Coupling strength of the links between oscillators.
        @param[in] frequency (double): Multiplier of internal frequency of the oscillators.
        @param[in] type_conn (conn_type): Type of connection between oscillators in the network (all-to-all, grid, bidirectional list, etc.).
        @param[in] representation (conn_represent): Internal representation of connection in the network: matrix or list.
        @param[in] initial_phases (initial_type): Type of initialization of initial phases of oscillators (random, uniformly distributed, etc.).
        @param[in] ccore (bool): If True simulation is performed by CCORE library (C++ implementation of pyclustering).
        
        """

        self._ccore_network_pointer = None;      # Pointer to CCORE Sync implementation of the network.

        if ( (ccore is True) and ccore_library.workable() ):
            self._ccore_network_pointer = wrapper.sync_create_network(num_osc, weight, frequency, type_conn, initial_phases);
            self._num_osc = num_osc;
            self._conn_represent = conn_represent.MATRIX;

        else:
            super().__init__(num_osc, type_conn, representation);

            self._weight = weight;

            self._phases = list();
            self._freq = list();

            random.seed();
            for index in range(0, num_osc, 1):
                if (initial_phases == initial_type.RANDOM_GAUSSIAN):
                    self._phases.append(random.random() * 2 * pi);

                elif (initial_phases == initial_type.EQUIPARTITION):
                    self._phases.append( pi / num_osc * index);

                self._freq.append(random.random() * frequency);


    def __del__(self):
        """!
        @brief Destructor of oscillatory network is based on Kuramoto model.
        
        """

        if (self._ccore_network_pointer is not None):
            wrapper.sync_destroy_network(self._ccore_network_pointer);
            self._ccore_network_pointer = None;


    def sync_order(self):
        """!
        @brief Calculates current level of global synchorization (order parameter) in the network.
        @details This parameter is tend 1.0 when the oscillatory network close to global synchronization and it tend to 0.0 when 
                  desynchronization is observed in the network. Order parameter is calculated using following equation:
                  
                  \f[
                  r_{c}=\frac{1}{Ne^{i\varphi }}\sum_{j=0}^{N}e^{i\theta_{j}};
                  \f]
                  
                  where \f$\varphi\f$ is a average phase coordinate in the network, \f$N\f$ is an amount of oscillators in the network.
        
        Example:
        @code
            oscillatory_network = sync(16, type_conn = conn_type.ALL_TO_ALL);
            output_dynamic = oscillatory_network.simulate_static(100, 10);
            
            if (oscillatory_network.sync_order() < 0.9): print("Global synchronization is not reached yet.");
            else: print("Global synchronization is reached.");
        @endcode
        
        @return (double) Level of global synchronization (order parameter).
        
        @see sync_local_order()
        
        """

        if (self._ccore_network_pointer is not None):
            return wrapper.sync_order(self._ccore_network_pointer);

        return order_estimator.calculate_sync_order(self._phases);


    def sync_local_order(self):
        """!
        @brief Calculates current level of local (partial) synchronization in the network.
        
        @return (double) Level of local (partial) synchronization.
        
        @see sync_order()
        
        """

        if (self._ccore_network_pointer is not None):
            return wrapper.sync_local_order(self._ccore_network_pointer);

        return order_estimator.calculate_local_sync_order(self._phases, self);


    def _phase_kuramoto(self, teta, t, argv):
        """!
        @brief Returns result of phase calculation for specified oscillator in the network.
        
        @param[in] teta (double): Phase of the oscillator that is differentiated.
        @param[in] t (double): Current time of simulation.
        @param[in] argv (tuple): Index of the oscillator in the list.
        
        @return (double) New phase for specified oscillator (don't assign here).
        
        """

        index = argv;
        phase = 0;
        for k in range(0, self._num_osc):
            if (self.has_connection(index, k) == True):
                phase += math.sin(self._phases[k] - teta);

        return ( self._freq[index] + (phase * self._weight / self._num_osc) );


    def simulate(self, steps, time, solution = solve_type.FAST, collect_dynamic = True):
        """!
        @brief Performs static simulation of Sync oscillatory network.
        
        @param[in] steps (uint): Number steps of simulations during simulation.
        @param[in] time (double): Time of simulation.
        @param[in] solution (solve_type): Type of solution (solving).
        @param[in] collect_dynamic (bool): If True - returns whole dynamic of oscillatory network, otherwise returns only last values of dynamics.
        
        @return (list) Dynamic of oscillatory network. If argument 'collect_dynamic' = True, than return dynamic for the whole simulation time,
                otherwise returns only last values (last step of simulation) of dynamic.
        
        @see simulate_dynamic()
        @see simulate_static()
        
        """

        return self.simulate_static(steps, time, solution, collect_dynamic);


    def simulate_dynamic(self, order = 0.998, solution = solve_type.FAST, collect_dynamic = False, step = 0.1, int_step = 0.01, threshold_changes = 0.0000001):
        """!
        @brief Performs dynamic simulation of the network until stop condition is not reached. Stop condition is defined by input argument 'order'.
        
        @param[in] order (double): Order of process synchronization, distributed 0..1.
        @param[in] solution (solve_type): Type of solution.
        @param[in] collect_dynamic (bool): If True - returns whole dynamic of oscillatory network, otherwise returns only last values of dynamics.
        @param[in] step (double): Time step of one iteration of simulation.
        @param[in] int_step (double): Integration step, should be less than step.
        @param[in] threshold_changes (double): Additional stop condition that helps prevent infinite simulation, defines limit of changes of oscillators between current and previous steps.
        
        @return (list) Dynamic of oscillatory network. If argument 'collect_dynamic' = True, than return dynamic for the whole simulation time,
                otherwise returns only last values (last step of simulation) of dynamic.
        
        @see simulate()
        @see simulate_static()
        
        """

        if (self._ccore_network_pointer is not None):
            ccore_instance_dynamic = wrapper.sync_simulate_dynamic(self._ccore_network_pointer, order, solution, collect_dynamic, step, int_step, threshold_changes);
            return sync_dynamic(None, None, ccore_instance_dynamic);

        # For statistics and integration
        time_counter = 0;

        # Prevent infinite loop. It's possible when required state cannot be reached.
        previous_order = 0;
        current_order = self.sync_local_order();

        # If requested input dynamics
        dyn_phase = [];
        dyn_time = [];
        if (collect_dynamic == True):
            dyn_phase.append(self._phases);
            dyn_time.append(0);

        # Execute until sync state will be reached
        while (current_order < order):
            # update states of oscillators
            self._phases = self._calculate_phases(solution, time_counter, step, int_step);

            # update time
            time_counter += step;

            # if requested input dynamic
            if (collect_dynamic == True):
                dyn_phase.append(self._phases);
                dyn_time.append(time_counter);

            # update orders
            previous_order = current_order;
            current_order = self.sync_local_order();

            # hang prevention
            if (abs(current_order - previous_order) < threshold_changes):
                # print("Warning: sync_network::simulate_dynamic - simulation is aborted due to low level of convergence rate (order = " + str(current_order) + ").");
                break;

        if (collect_dynamic != True):
            dyn_phase.append(self._phases);
            dyn_time.append(time_counter);

        output_sync_dynamic = sync_dynamic(dyn_phase, dyn_time, None);
        return output_sync_dynamic;


    def simulate_static(self, steps, time, solution = solve_type.FAST, collect_dynamic = False):
        """!
        @brief Performs static simulation of oscillatory network.
        
        @param[in] steps (uint): Number steps of simulations during simulation.
        @param[in] time (double): Time of simulation.
        @param[in] solution (solve_type): Type of solution.
        @param[in] collect_dynamic (bool): If True - returns whole dynamic of oscillatory network, otherwise returns only last values of dynamics.
        
        @return (list) Dynamic of oscillatory network. If argument 'collect_dynamic' = True, than return dynamic for the whole simulation time,
                otherwise returns only last values (last step of simulation) of dynamic.
        
        @see simulate()
        @see simulate_dynamic()
        
        """

        if (self._ccore_network_pointer is not None):
            ccore_instance_dynamic = wrapper.sync_simulate_static(self._ccore_network_pointer, steps, time, solution, collect_dynamic);
            return sync_dynamic(None, None, ccore_instance_dynamic);

        dyn_phase = [];
        dyn_time = [];

        if (collect_dynamic == True):
            dyn_phase.append(self._phases);
            dyn_time.append(0);

        step = time / steps;
        int_step = step / 10.0;

        for t in numpy.arange(step, time + step, step):
            # update states of oscillators
            self._phases = self._calculate_phases(solution, t, step, int_step);

            # update states of oscillators
            if (collect_dynamic == True):
                dyn_phase.append(self._phases);
                dyn_time.append(t);

        if (collect_dynamic != True):
            dyn_phase.append(self._phases);
            dyn_time.append(time);

        output_sync_dynamic = sync_dynamic(dyn_phase, dyn_time);
        return output_sync_dynamic;


    def _calculate_phases(self, solution, t, step, int_step):
        """!
        @brief Calculates new phases for oscillators in the network in line with current step.
        
        @param[in] solution (solve_type): Type solver of the differential equation.
        @param[in] t (double): Time of simulation.
        @param[in] step (double): Step of solution at the end of which states of oscillators should be calculated.
        @param[in] int_step (double): Step differentiation that is used for solving differential equation.
        
        @return (list) New states (phases) for oscillators.
        
        """

        next_phases = [0.0] * self._num_osc;    # new oscillator _phases

        for index in range (0, self._num_osc, 1):
            if (solution == solve_type.FAST):
                result = self._phases[index] + self._phase_kuramoto(self._phases[index], 0, index);
                next_phases[index] = self._phase_normalization(result);

            elif ( (solution == solve_type.RK4) or (solution == solve_type.RKF45) ):
                result = odeint(self._phase_kuramoto, self._phases[index], numpy.arange(t - step, t, int_step), (index , ));
                next_phases[index] = self._phase_normalization(result[len(result) - 1][0]);

            else:
                raise NameError("Solver '" + str(solution) + "' is not supported");

        return next_phases;


    def _phase_normalization(self, teta):
        """!
        @brief Normalization of phase of oscillator that should be placed between [0; 2 * pi].
        
        @param[in] teta (double): phase of oscillator.
        
        @return (double) Normalized phase.
        
        """

        norm_teta = teta;
        while (norm_teta > (2.0 * pi)) or (norm_teta < 0):
            if (norm_teta > (2.0 * pi)):
                norm_teta -= 2.0 * pi;
            else:
                norm_teta += 2.0 * pi;

        return norm_teta;


    def get_neighbors(self, index):
        """!
        @brief Finds neighbors of the oscillator with specified index.
        
        @param[in] index (uint): index of oscillator for which neighbors should be found in the network.
        
        @return (list) Indexes of neighbors of the specified oscillator.
        
        """

        if ( (self._ccore_network_pointer is not None) and (self._osc_conn is None) ):
            self._osc_conn = wrapper.sync_connectivity_matrix(self._ccore_network_pointer);

        return super().get_neighbors(index);


    def has_connection(self, i, j):
        """!
        @brief Returns True if there is connection between i and j oscillators and False - if connection doesn't exist.
        
        @param[in] i (uint): index of an oscillator in the network.
        @param[in] j (uint): index of an oscillator in the network.
        
        """

        if ( (self._ccore_network_pointer is not None) and (self._osc_conn is None) ):
            self._osc_conn = wrapper.sync_connectivity_matrix(self._ccore_network_pointer);

        return super().has_connection(i, j);