complexeva 0.1.6__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {complexeva-0.1.6 → complexeva-0.1.7}/PKG-INFO +1 -1
  2. complexeva-0.1.7/complexeva/measures/dimension_1/__init__.py +0 -0
  3. complexeva-0.1.7/complexeva/measures/dimension_1/higuchi.py +91 -0
  4. complexeva-0.1.7/complexeva/measures/dimension_1/lempel_ziv.py +201 -0
  5. complexeva-0.1.7/complexeva/measures/dimension_2/__init__.py +0 -0
  6. complexeva-0.1.7/complexeva/measures/dimension_2/cbb_matrix.py +134 -0
  7. complexeva-0.1.7/complexeva/measures/network_space/__init__.py +0 -0
  8. complexeva-0.1.7/complexeva/measures/network_space/cbb_networkx.py +288 -0
  9. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva.egg-info/PKG-INFO +1 -1
  10. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva.egg-info/SOURCES.txt +7 -0
  11. {complexeva-0.1.6 → complexeva-0.1.7}/setup.py +1 -1
  12. {complexeva-0.1.6 → complexeva-0.1.7}/README.md +0 -0
  13. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/__init__.py +0 -0
  14. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/dummies/__init__.py +0 -0
  15. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/dummies/matrix.py +0 -0
  16. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/dummies/networks.py +0 -0
  17. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/measures/__init__.py +0 -0
  18. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/measures/validate.py +0 -0
  19. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/utils/__init__.py +0 -0
  20. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/utils/convert_image_matrix.py +0 -0
  21. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/utils/convert_matrix_network.py +0 -0
  22. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva/utils/device.py +0 -0
  23. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva.egg-info/dependency_links.txt +0 -0
  24. {complexeva-0.1.6 → complexeva-0.1.7}/complexeva.egg-info/top_level.txt +0 -0
  25. {complexeva-0.1.6 → complexeva-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: complexeva
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Home-page:
5
5
  Author: Don Yin
6
6
  Author-email: don_yin@outlook.com
@@ -0,0 +1,91 @@
1
+ #!/Users/donyin/miniconda3/envs/rotation-1/bin/python
2
+
3
+
4
+ import numpy
5
+ from scipy.fft import fft, ifft
6
+ import matplotlib.pyplot as plt
7
+ from donware import inspect_package
8
+ from numba import njit
9
+
10
+
11
+ def hfd_matlab_equivalent(data, k_max=16):
12
+ """
13
+ [translated from MATLAB code]
14
+
15
+ [output]
16
+ - produce values between 1 and 2
17
+ """
18
+ N = len(data)
19
+ L = numpy.zeros(k_max)
20
+ x = numpy.zeros(k_max)
21
+ y = numpy.zeros(k_max)
22
+
23
+ for k in range(1, k_max + 1):
24
+ Lk = numpy.zeros(k)
25
+ for m in range(1, k + 1):
26
+ norm_factor = (N - 1) / (numpy.round((N - m) / k) * k)
27
+ X = numpy.sum(numpy.abs(numpy.diff(data[m - 1 :: k])))
28
+ Lk[m - 1] = X * norm_factor / k
29
+
30
+ y[k - 1] = numpy.log(numpy.sum(Lk) / k)
31
+ x[k - 1] = numpy.log(1 / k)
32
+
33
+ D = numpy.polyfit(x, y, 1)
34
+ HFD = D[0]
35
+
36
+ return HFD
37
+
38
+
39
+ @njit
40
+ def compute_L_x(X, k_max=16):
41
+ N = len(X)
42
+ L = numpy.zeros(k_max)
43
+ x = numpy.zeros(k_max)
44
+ for k in range(1, k_max + 1):
45
+ Lk = numpy.zeros(k)
46
+ for m in range(k):
47
+ Lmk = 0.0
48
+ n_max = (N - m) // k
49
+ for i in range(1, n_max):
50
+ Lmk += abs(X[m + i * k] - X[m + (i - 1) * k])
51
+ Lmk *= (N - 1) / (n_max * k)
52
+ Lk[m] = Lmk
53
+ L[k - 1] = numpy.log(Lk.mean())
54
+ x[k - 1] = numpy.log(1.0 / k)
55
+ return x, L
56
+
57
+
58
+ def hfd_pyeeg(X, k_max=16):
59
+ x, L = compute_L_x(X, k_max)
60
+ A = numpy.column_stack((x, numpy.ones_like(x)))
61
+ beta, _, _, _ = numpy.linalg.lstsq(A, L, rcond=None)
62
+ return beta[0]
63
+
64
+
65
+ def _hfd_pyeeg(X, k_max=16):
66
+ """
67
+ [from PyEEG package]
68
+ Higuchi Fractal Dimension of a time series X. kmax is an HFD parameter
69
+
70
+ [output]
71
+ - for some reasons produce values between 0 and 1
72
+
73
+ [modifications]
74
+ - kmax should be inclusive, Kmax + 1
75
+ """
76
+ L = []
77
+ x = []
78
+ N = len(X)
79
+ for k in range(1, k_max + 1):
80
+ Lk = []
81
+ for m in range(0, k):
82
+ Lmk = 0
83
+ for i in range(1, int(numpy.floor((N - m) / k))):
84
+ Lmk += abs(X[m + i * k] - X[m + i * k - k])
85
+ Lmk = Lmk * (N - 1) / numpy.floor((N - m) / float(k)) / k
86
+ Lk.append(Lmk)
87
+ L.append(numpy.log(numpy.mean(Lk)))
88
+ x.append([numpy.log(float(1) / k), 1])
89
+
90
+ (p, _, _, _) = numpy.linalg.lstsq(x, L, rcond=None)
91
+ return p[0]
@@ -0,0 +1,201 @@
1
+ #!/Users/donyin/miniconda3/envs/rotation-1/bin/python
2
+
3
+ import numpy
4
+
5
+
6
+ def lempel_ziv(time_series, method="median"):
7
+ """
8
+ - compared to other implementations, this implementation is makes the most sense
9
+
10
+ Lempel-Ziv complexity as described in Kaspar and Schuster, Phys. Rev. A.
11
+ Counts the number of distinct patterns that need to be copied to reproduce the sequence.
12
+ Returns: int: The number of distinct patterns (complexity measure)
13
+ """
14
+ sequence = time_series_to_binary(time_series, method=method)
15
+
16
+ history_pos = 0 # Position in history we're comparing against
17
+ current_len = 1 # Length of current pattern we're checking
18
+ max_pattern_len = 1 # Length of longest matching pattern found
19
+ seq_pos = 1 # Current position in sequence
20
+ complexity = 1 # Start with 1 as first character is always a new pattern
21
+ seq_len = len(sequence)
22
+
23
+ while seq_pos + current_len <= seq_len:
24
+ # Check if current pattern matches what's in history
25
+ if sequence[history_pos + current_len - 1] == sequence[seq_pos + current_len - 1]:
26
+ current_len += 1
27
+ else:
28
+ # Update longest matching pattern length if needed
29
+ if current_len > max_pattern_len:
30
+ max_pattern_len = current_len
31
+
32
+ history_pos += 1
33
+
34
+ # If we've checked all history, we found a new pattern
35
+ if history_pos == seq_pos:
36
+ complexity += 1
37
+ seq_pos += max_pattern_len
38
+
39
+ # Reset for next pattern search
40
+ history_pos = 0
41
+ current_len = 1
42
+ max_pattern_len = 1
43
+ else:
44
+ # Try next position in history
45
+ current_len = 1
46
+
47
+ return complexity
48
+
49
+
50
+ def time_series_to_binary(vector, method="median"):
51
+ """
52
+ - convert a time series to a binary sequence based on a threshold.
53
+ - returns: str: A binary sequence as a string of '0's and '1's.
54
+ - https://doi.org/10.1101/2021.09.23.461002 this paper used median, making median the default method.
55
+ - diff also makes somewhat sense
56
+ """
57
+
58
+ vector = numpy.array(vector)
59
+
60
+ if method == "mean":
61
+ threshold = numpy.mean(vector)
62
+ binary_seq = "".join(["1" if x > threshold else "0" for x in vector])
63
+ elif method == "median":
64
+ threshold = numpy.median(vector)
65
+ binary_seq = "".join(["1" if x > threshold else "0" for x in vector])
66
+ elif method == "diff":
67
+ diffs = numpy.diff(
68
+ vector
69
+ ) # this measures whether the value is increasing or decreasing as compared to the previous value
70
+ binary_seq = "".join(["1" if x > 0 else "0" for x in diffs])
71
+ else:
72
+ raise ValueError("Method must be 'mean', 'median', or 'diff'.")
73
+
74
+ return binary_seq
75
+
76
+
77
+ def lempel_ziv_v1(time_series: numpy.ndarray) -> int:
78
+ r"""Manual implementation of the Lempel-Ziv complexity.
79
+
80
+ It is defined as the number of different substrings encountered as the stream is viewed from begining to the end.
81
+ As an example:
82
+
83
+ >>> s = '1001111011000010'
84
+ >>> lempel_ziv_complexity(s) # 1 / 0 / 01 / 11 / 10 / 110 / 00 / 010
85
+ 8
86
+
87
+ Marking in the different substrings the sequence complexity :math:`\mathrm{Lempel-Ziv}(s) = 8`: :math:`s = 1 / 0 / 01 / 11 / 10 / 110 / 00 / 010`.
88
+
89
+ - See the page https://en.wikipedia.org/wiki/Lempel-Ziv_complexity for more details.
90
+
91
+
92
+ Other examples:
93
+
94
+ >>> lempel_ziv_complexity('1010101010101010') # 1, 0, 10, 101, 01, 010, 1010
95
+ 7
96
+ >>> lempel_ziv_complexity('1001111011000010000010') # 1, 0, 01, 11, 10, 110, 00, 010, 000
97
+ 9
98
+ >>> lempel_ziv_complexity('100111101100001000001010') # 1, 0, 01, 11, 10, 110, 00, 010, 000, 0101
99
+ 10
100
+
101
+ - Note: it is faster to give the sequence as a string of characters, like `'10001001'`, instead of a list or a numpy array.
102
+ - Note: see this notebook for more details, comparison, benchmarks and experiments: https://Nbviewer.Jupyter.org/github/Naereen/Lempel-Ziv_Complexity/Short_study_of_the_Lempel-Ziv_complexity.ipynb
103
+ - Note: there is also a Cython-powered version, for speedup, see :download:`lempel_ziv_complexity_cython.pyx`.
104
+ """
105
+
106
+ sequence = time_series_to_binary(time_series, method="median")
107
+
108
+ sub_strings = set()
109
+
110
+ ind = 0
111
+ inc = 1
112
+ while True:
113
+ if ind + inc > len(sequence):
114
+ break
115
+ sub_str = sequence[ind : ind + inc]
116
+ if sub_str in sub_strings:
117
+ inc += 1
118
+ else:
119
+ sub_strings.add(sub_str)
120
+ ind += inc
121
+ inc = 1
122
+ return len(sub_strings)
123
+
124
+
125
+ def lempel_ziv_v2(time_series):
126
+ """
127
+ Compute the Lempel-Ziv complexity of a binary sequence.
128
+
129
+ Parameters:
130
+ sequence (str): The binary sequence as a string of '0' and '1'.
131
+
132
+ Returns:
133
+ int: The Lempel-Ziv complexity.
134
+ """
135
+ sequence = time_series_to_binary(time_series, method="median")
136
+
137
+ sub_strings = set()
138
+ n = len(sequence)
139
+ ind = 0
140
+ count = 0
141
+
142
+ while ind < n:
143
+ inc = 1
144
+ # Find the shortest substring not already in sub_strings
145
+ while sequence[ind : ind + inc] in sub_strings and ind + inc <= n:
146
+ inc += 1
147
+
148
+ # Add the new substring to the set
149
+ sub_strings.add(sequence[ind : ind + inc])
150
+ count += 1
151
+ ind += inc
152
+
153
+ return count
154
+
155
+
156
+ def lempel_ziv_v4(time_series):
157
+ """
158
+ Computes the Lempel-Ziv complexity of the input sequence s.
159
+
160
+ Parameters:
161
+ s (str): The input sequence (e.g., a binary string).
162
+
163
+ Returns:
164
+ int: The Lempel-Ziv complexity of the sequence.
165
+ """
166
+
167
+ sequence = time_series_to_binary(time_series, method="median")
168
+ n = len(sequence)
169
+ i = 0
170
+ c = 1 # Complexity starts at 1 since the first character is always new
171
+ k = 1
172
+ l = 1
173
+
174
+ while l + k <= n:
175
+ # Check if the substring s[l:l+k] has occurred in s[0:l]
176
+ if sequence[l : l + k] in sequence[0:l]:
177
+ k += 1
178
+ if l + k - 1 == n:
179
+ c += 1
180
+ break
181
+ else:
182
+ c += 1
183
+ l = l + k
184
+ k = 1
185
+
186
+ return c
187
+
188
+
189
+ if __name__ == "__main__":
190
+ sequence = "1001111011000010"
191
+ print(lempel_ziv_v1(sequence))
192
+ print(lempel_ziv_v2(sequence))
193
+ print(lempel_ziv(sequence))
194
+ print(lempel_ziv_v4(sequence))
195
+
196
+ signal = numpy.random.rand(1000)
197
+ binary_sequence = time_series_to_binary(signal, method="median")
198
+ print(lempel_ziv_v1(binary_sequence))
199
+ print(lempel_ziv_v2(binary_sequence))
200
+ print(lempel_ziv(binary_sequence))
201
+ print(lempel_ziv_v4(binary_sequence))
@@ -0,0 +1,134 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from matplotlib import animation
4
+
5
+
6
+ class FractalHandlerMatrix:
7
+ """
8
+ Handles the fractal dimension calculation of a binary matrix using an improved Compact Box-Burning algorithm.
9
+ """
10
+
11
+ def __init__(self, matrix: np.ndarray):
12
+ self.matrix = matrix
13
+
14
+ def compute_fractal_dimension(self, save_plot_as=None, save_animated_as=None):
15
+ """
16
+ Calculate the fractal dimension of the matrix using the improved Compact Box-Burning algorithm.
17
+
18
+ Parameters:
19
+ save_plot_as (str, optional): Path to save the static plot showing fractal dimension calculation
20
+ save_animated_as (str, optional): Path to save the animated GIF showing box counting process
21
+
22
+ Returns:
23
+ float: The fractal dimension of the matrix.
24
+ """
25
+ assert np.all((self.matrix == 0) | (self.matrix == 1)), "> warning: Matrix should be binary (0s and 1s only)"
26
+ binary_matrix = self.matrix.astype(np.uint8)
27
+ min_dim = min(binary_matrix.shape)
28
+ max_scale = 2 ** int(np.floor(np.log2(min_dim)))
29
+
30
+ num_scales = int(np.floor(np.log2(max_scale))) * 10
31
+ scales = np.unique(np.floor(np.logspace(0, np.log2(max_scale), num=num_scales, base=2))).astype(int)
32
+ scales = scales[scales >= 1]
33
+
34
+ box_counts = []
35
+ padded_matrices = []
36
+
37
+ # Animation setup if needed
38
+ if save_animated_as:
39
+ fig, ax = plt.subplots(figsize=(8, 8))
40
+
41
+ for scale in scales:
42
+ padded_size = (
43
+ int(np.ceil(binary_matrix.shape[0] / scale)) * scale,
44
+ int(np.ceil(binary_matrix.shape[1] / scale)) * scale,
45
+ )
46
+ padded_matrix = np.zeros(padded_size, dtype=np.uint8)
47
+ padded_matrix[: binary_matrix.shape[0], : binary_matrix.shape[1]] = binary_matrix
48
+
49
+ if save_animated_as:
50
+ padded_matrices.append((padded_matrix, scale))
51
+
52
+ reshaped = padded_matrix.reshape(padded_size[0] // scale, scale, padded_size[1] // scale, scale)
53
+ summed = reshaped.sum(axis=(1, 3))
54
+ occupied_boxes = np.count_nonzero(summed)
55
+ box_counts.append(occupied_boxes)
56
+
57
+ # calculate fractal dimension
58
+ scales = np.array(scales)
59
+ box_counts = np.array(box_counts)
60
+ nonzero = box_counts > 0
61
+ scales = scales[nonzero]
62
+ box_counts = box_counts[nonzero]
63
+
64
+ log_scales = np.log(1 / scales)
65
+ log_box_counts = np.log(box_counts)
66
+
67
+ coeffs = np.polyfit(log_scales, log_box_counts, 1)
68
+ fractal_dimension = coeffs[0]
69
+
70
+ # save static plot if requested
71
+ if save_plot_as:
72
+ save_plot_as = save_plot_as if save_plot_as.endswith(".png") else save_plot_as + ".png"
73
+ plt.figure(figsize=(8, 6))
74
+ plt.plot(log_scales, log_box_counts, "o-", label="Data")
75
+ plt.plot(
76
+ log_scales,
77
+ np.polyval(coeffs, log_scales),
78
+ "r--",
79
+ label=f"Fit (D = {fractal_dimension:.4f})",
80
+ )
81
+ plt.xlabel("log(1/ε)")
82
+ plt.ylabel("log(N(ε))")
83
+ plt.title("Fractal Dimension via Compact Box-Burning")
84
+ plt.legend()
85
+ plt.grid(True)
86
+ plt.tight_layout()
87
+ plt.savefig(save_plot_as, dpi=420, bbox_inches="tight")
88
+ plt.close()
89
+
90
+ # Create and save animation if requested
91
+ if save_animated_as:
92
+
93
+ def init():
94
+ ax.imshow(binary_matrix, cmap="binary", interpolation="nearest")
95
+ ax.axis("off")
96
+ return []
97
+
98
+ def update(frame):
99
+ padded_matrix, scale = padded_matrices[frame]
100
+ ax.clear()
101
+ ax.imshow(padded_matrix, cmap="binary", interpolation="nearest")
102
+ ax.set_title(f"Scale: {scale} pixels, Boxes: {box_counts[frame]}")
103
+ ax.axis("off")
104
+
105
+ # Overlay red boxes on occupied regions
106
+ for y in range(0, padded_matrix.shape[0], scale):
107
+ for x in range(0, padded_matrix.shape[1], scale):
108
+ if np.any(padded_matrix[y : y + scale, x : x + scale]):
109
+ rect = plt.Rectangle(
110
+ (x - 0.5, y - 0.5),
111
+ scale,
112
+ scale,
113
+ edgecolor="red",
114
+ facecolor="none",
115
+ lw=1,
116
+ )
117
+ ax.add_patch(rect)
118
+ return []
119
+
120
+ ani = animation.FuncAnimation(
121
+ fig,
122
+ update,
123
+ frames=len(scales),
124
+ init_func=init,
125
+ blit=False,
126
+ repeat=False,
127
+ )
128
+
129
+ if not save_animated_as.endswith((".png", ".gif")):
130
+ save_animated_as += ".gif"
131
+ ani.save(save_animated_as.replace(".png", ".gif"), writer="pillow", fps=2)
132
+ plt.close(fig)
133
+
134
+ return fractal_dimension
@@ -0,0 +1,288 @@
1
+ #!/Users/donyin/miniconda3/bin/python
2
+ import random
3
+ from PIL import Image
4
+ import numpy as np
5
+ import networkx as nx
6
+ from tqdm import tqdm
7
+ from scipy import stats
8
+ import matplotlib.pyplot as plt
9
+ from networkx.exception import NetworkXError
10
+ import matplotlib.animation as animation
11
+
12
+
13
+ """
14
+ reference:
15
+ - https://github.com/PeterTKovacs/boxes/blob/main/boxes/cbb.py
16
+ """
17
+
18
+
19
+ class FractalHandlerNetworkX:
20
+ """
21
+ Handles fractal analysis of NetworkX graphs using box-covering method.
22
+ """
23
+
24
+ def __init__(self, network: nx.Graph):
25
+ self.network = network
26
+
27
+ def auto_pick_box_sizes(self):
28
+ diameter = nx.diameter(self.network) if nx.is_connected(self.network) else 1
29
+ min_size = max(1, diameter // 21)
30
+ max_size = diameter
31
+ return np.logspace(np.log10(min_size), np.log10(max_size), num=12, dtype=int)
32
+
33
+ def compute_fractal_dimensions(self, box_sizes="auto", iterations=10, save_plot_as=None, save_animated_as=None):
34
+ box_sizes = self.auto_pick_box_sizes() if box_sizes == "auto" else box_sizes
35
+ network_image, box_counts = self.plot_network(), []
36
+
37
+ # If animation is requested, set up storage for animation frames
38
+ if save_animated_as:
39
+ animation_data = []
40
+ pos = nx.spring_layout(self.network, dim=3) # 3D layout
41
+
42
+ # Create a color mapping based on z-coordinates
43
+ z_coords = np.array([pos[node][2] for node in self.network.nodes()])
44
+ z_min, z_max = z_coords.min(), z_coords.max()
45
+ node_colors = {}
46
+ for node in self.network.nodes():
47
+ z = pos[node][2]
48
+ # Normalize z to [0,1] range for color mapping
49
+ color_val = (z - z_min) / (z_max - z_min) if z_max != z_min else 0
50
+ node_colors[node] = plt.cm.rainbow(color_val)
51
+
52
+ for max_distance in tqdm(box_sizes, desc="Box sizes"):
53
+ num_boxes_list = []
54
+ boxes_for_animation = []
55
+ for _ in tqdm(range(iterations), desc=f"Networks for size {max_distance}", leave=False):
56
+ boxes = self.random_box_covering(max_distance)
57
+ num_boxes_list.append(len(boxes))
58
+ if save_animated_as:
59
+ boxes_for_animation.append(boxes)
60
+ avg_num_boxes = sum(num_boxes_list) / iterations
61
+ box_counts.append(avg_num_boxes)
62
+
63
+ if save_animated_as:
64
+ # For animation, store the boxes from the first iteration
65
+ animation_data.append(
66
+ {
67
+ "boxes": boxes_for_animation[0],
68
+ "max_distance": max_distance,
69
+ "num_boxes": num_boxes_list[0],
70
+ }
71
+ )
72
+
73
+ log_box_sizes, log_counts = np.log(box_sizes), np.log(box_counts)
74
+
75
+ try:
76
+ slope, intercept, r_value, p_value, std_err = stats.linregress(log_box_sizes, log_counts)
77
+ except ValueError:
78
+ slope, intercept, r_value = 0, 0, 0
79
+ fractal_dimension = -slope
80
+
81
+ if save_plot_as:
82
+ save_plot_as = save_plot_as if save_plot_as.endswith(".png") else f"{save_plot_as}.png"
83
+ fig, ax = plt.subplots(figsize=(10, 6))
84
+ ax.scatter(log_box_sizes, log_counts, color="blue", label="Data")
85
+ ax.plot(
86
+ log_box_sizes,
87
+ slope * log_box_sizes + intercept,
88
+ color="red",
89
+ label=f"Fit (D={fractal_dimension:.2f})",
90
+ linestyle="-",
91
+ )
92
+ ax.set_xlabel("log(Box Size)", fontsize=21)
93
+ ax.set_ylabel("log(Number of Boxes)", fontsize=21)
94
+ ax.set_title("Fractal Dimension Regression", fontsize=21)
95
+ ax.legend(fontsize=21)
96
+ ax.grid(True)
97
+ ax.tick_params(axis="both", which="major", labelsize=21)
98
+ fig.canvas.draw()
99
+ plot_image = np.array(fig.canvas.renderer.buffer_rgba())
100
+ network_height, network_width = network_image.shape[:2]
101
+ plot_height, plot_width = plot_image.shape[:2]
102
+ aspect_ratio = network_width / network_height
103
+ new_network_height = plot_height
104
+ new_network_width = int(new_network_height * aspect_ratio)
105
+ resized_network_image = np.array(Image.fromarray(network_image).resize((new_network_width, new_network_height)))
106
+ resized_network_image = resized_network_image[:, :, :3]
107
+ plot_image = plot_image[:, :, :3]
108
+ combined_image = np.hstack((resized_network_image, plot_image))
109
+ plt.figure(
110
+ figsize=(
111
+ combined_image.shape[1] / 100,
112
+ combined_image.shape[0] / 100,
113
+ )
114
+ )
115
+ plt.imshow(combined_image)
116
+ plt.axis("off")
117
+ plt.tight_layout()
118
+ plt.savefig(save_plot_as, dpi=420, bbox_inches="tight")
119
+ plt.close()
120
+
121
+ # Create and save animation if requested
122
+ if save_animated_as:
123
+ fig = plt.figure(figsize=(15, 8)) # Wider figure to accommodate legend
124
+ gs = plt.GridSpec(1, 2, width_ratios=[2, 1]) # 2:1 ratio for plot and legend
125
+ ax = fig.add_subplot(gs[0], projection="3d")
126
+ legend_ax = fig.add_subplot(gs[1]) # Create legend subplot once
127
+ legend_ax.axis("off")
128
+ legend_ax.spines["top"].set_visible(False)
129
+
130
+ def update(frame):
131
+ ax.clear()
132
+ legend_ax.clear() # Clear previous legend
133
+
134
+ data = animation_data[frame]
135
+ boxes = data["boxes"]
136
+ max_distance = data["max_distance"]
137
+ num_boxes = data["num_boxes"]
138
+
139
+ # Draw network and boxes in 3D subplot
140
+ edge_list = list(self.network.edges())
141
+
142
+ # Assign box colors based on average z-coordinate of nodes in each box
143
+ box_colors = []
144
+ for box in boxes:
145
+ box_nodes = list(box)
146
+ box_colors.append(np.mean([node_colors[node] for node in box_nodes], axis=0))
147
+
148
+ # Create a mapping of nodes to their box colors
149
+ node_to_color = {}
150
+ for box, color in zip(boxes, box_colors):
151
+ for node in box:
152
+ node_to_color[node] = color
153
+
154
+ # Draw edges with box colors
155
+ for edge in edge_list:
156
+ start = pos[edge[0]]
157
+ end = pos[edge[1]]
158
+ # Use color of both nodes for edge
159
+ start_color = node_to_color.get(edge[0], "gray")
160
+ end_color = node_to_color.get(edge[1], "gray")
161
+ edge_color = start_color if np.array_equal(start_color, end_color) else "gray"
162
+
163
+ mid_point = (start + end) / 2
164
+ offset = np.random.normal(0, 0.05, 3)
165
+ control_point = mid_point + offset
166
+ t = np.linspace(0, 1, 20)
167
+ curve = np.array([(1 - t) ** 2 * start + 2 * (1 - t) * t * control_point + t**2 * end for t in t])
168
+ ax.plot3D(curve[:, 0], curve[:, 1], curve[:, 2], c=edge_color, alpha=0.7, linewidth=1.5)
169
+
170
+ # Draw boxes and create legend elements
171
+ legend_elements = []
172
+ for box_idx, (box, color) in enumerate(zip(boxes, box_colors)):
173
+ box_nodes = list(box)
174
+ box_pos = np.array([pos[node] for node in box_nodes])
175
+ scatter = ax.scatter(
176
+ box_pos[:, 0], box_pos[:, 1], box_pos[:, 2], c=[color], s=150, alpha=0.8, edgecolor="white"
177
+ )
178
+ legend_elements.append((scatter, f"Box {box_idx+1}"))
179
+
180
+ # Set 3D plot properties
181
+ ax.set_title(f"Step {frame+1}/{len(animation_data)}\nBox Size = {max_distance}\nBoxes Required: {num_boxes}")
182
+ ax.set_xlabel("X")
183
+ ax.set_ylabel("Y")
184
+ ax.set_zlabel("Z")
185
+
186
+ # Calculate optimal number of columns based on number of boxes
187
+ n_boxes = len(boxes)
188
+ n_cols = min(4, n_boxes) # Max 4 columns for more spacing
189
+ n_rows = (n_boxes + n_cols - 1) // n_cols # Ceiling division
190
+
191
+ # Create legend with more spacing
192
+ legend = legend_ax.legend(
193
+ *zip(*legend_elements),
194
+ loc="center",
195
+ ncol=n_cols,
196
+ bbox_to_anchor=(0.5, 0.5),
197
+ borderaxespad=1,
198
+ labelspacing=1.5, # Increase vertical spacing between legend entries
199
+ handletextpad=1.2, # Increase spacing between marker and text
200
+ title="Box Groups",
201
+ frameon=True, # Add frame around legend
202
+ fancybox=True, # Round corners
203
+ shadow=True, # Add shadow
204
+ )
205
+ legend.get_title().set_fontsize(14)
206
+ for text in legend.get_texts():
207
+ text.set_fontsize(12)
208
+
209
+ ax.set_box_aspect([1, 1, 1])
210
+ ax.view_init(elev=20, azim=45)
211
+ ax.dist = 8
212
+
213
+ ani = animation.FuncAnimation(fig, update, frames=len(animation_data), interval=2000, repeat=True)
214
+
215
+ save_animated_as = save_animated_as if save_animated_as.endswith((".png", ".gif")) else save_animated_as + ".gif"
216
+ ani.save(save_animated_as.replace(".png", ".gif"), writer="pillow", fps=0.5)
217
+ plt.close(fig)
218
+
219
+ return fractal_dimension, r_value**2, log_box_sizes, log_counts
220
+
221
+ # ---- functional helpers ----
222
+ def random_box_covering(self, max_distance: int):
223
+ """
224
+ [NOTE] This implementation handles disconnected graphs by processing each connected component separately.
225
+
226
+ This function implements the random box covering algorithm to estimate
227
+ the boxes needed to cover a network at a given maximum distance.
228
+
229
+ Args:
230
+ max_distance (int): The maximum distance allowed within each box, in number of hops.
231
+
232
+ Returns:
233
+ List[Set]: A list of sets, each containing the nodes in a box.
234
+ """
235
+ boxes = []
236
+ for component in nx.connected_components(self.network):
237
+ subgraph = self.network.subgraph(component)
238
+ uncovered_nodes = set(subgraph.nodes())
239
+ while uncovered_nodes:
240
+ p = random.choice(list(uncovered_nodes))
241
+ distances = nx.single_source_shortest_path_length(subgraph, p)
242
+ box_nodes = set(node for node, dist in distances.items() if dist <= max_distance).intersection(uncovered_nodes)
243
+ boxes.append(box_nodes)
244
+ uncovered_nodes -= box_nodes
245
+ return boxes
246
+
247
+ def plot_network(self):
248
+ pos = nx.get_node_attributes(self.network, "pos")
249
+ fig = plt.figure(figsize=(10, 8))
250
+ ax = fig.add_subplot(111, projection="3d")
251
+
252
+ if pos:
253
+ # Use provided positions if they exist
254
+ pos_3d = {node: (x, y, 0) for node, (x, y) in pos.items()} # Convert 2D to 3D
255
+ else:
256
+ # Generate 3D spring layout
257
+ pos_3d = nx.spring_layout(self.network, dim=3)
258
+
259
+ # Draw edges with curves
260
+ for edge in self.network.edges():
261
+ start = np.array(pos_3d[edge[0]])
262
+ end = np.array(pos_3d[edge[1]])
263
+ ax.plot([start[0], end[0]], [start[1], end[1]], [start[2], end[2]], "gray", alpha=0.5)
264
+
265
+ # Draw nodes
266
+ node_pos = np.array([pos_3d[node] for node in self.network.nodes()])
267
+ ax.scatter(node_pos[:, 0], node_pos[:, 1], node_pos[:, 2], c="lightblue", s=30)
268
+
269
+ ax.set_title("3D Network Visualization")
270
+ ax.set_xlabel("X")
271
+ ax.set_ylabel("Y")
272
+ ax.set_zlabel("Z")
273
+
274
+ plt.tight_layout()
275
+ fig.canvas.draw()
276
+ image = np.array(fig.canvas.renderer.buffer_rgba())
277
+ plt.close(fig)
278
+ return image
279
+
280
+
281
+ if __name__ == "__main__":
282
+ from src.dummies.networks import WebFractalNetwork, SierpinskiFractal
283
+
284
+ network = SierpinskiFractal(recursion_depth=6)
285
+ network = WebFractalNetwork(complexity=4)
286
+
287
+ handler = FractalHandlerNetworkX(network.network)
288
+ handler.compute_fractal_dimensions(box_sizes=np.linspace(1, 10, 20), iterations=10)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: complexeva
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Home-page:
5
5
  Author: Don Yin
6
6
  Author-email: don_yin@outlook.com
@@ -10,6 +10,13 @@ complexeva/dummies/matrix.py
10
10
  complexeva/dummies/networks.py
11
11
  complexeva/measures/__init__.py
12
12
  complexeva/measures/validate.py
13
+ complexeva/measures/dimension_1/__init__.py
14
+ complexeva/measures/dimension_1/higuchi.py
15
+ complexeva/measures/dimension_1/lempel_ziv.py
16
+ complexeva/measures/dimension_2/__init__.py
17
+ complexeva/measures/dimension_2/cbb_matrix.py
18
+ complexeva/measures/network_space/__init__.py
19
+ complexeva/measures/network_space/cbb_networkx.py
13
20
  complexeva/utils/__init__.py
14
21
  complexeva/utils/convert_image_matrix.py
15
22
  complexeva/utils/convert_matrix_network.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="complexeva",
5
- version="0.1.6",
5
+ version="0.1.7",
6
6
  packages=find_packages(),
7
7
  install_requires=[],
8
8
  author="Don Yin",
File without changes
File without changes