complexeva 0.1.5__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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: complexeva
3
+ Version: 0.1.5
4
+ Home-page:
5
+ Author: Don Yin
6
+ Author-email: don_yin@outlook.com
7
+ Description-Content-Type: text/markdown
8
+
9
+ # overview
10
+ computing fractal dimensions (and other complexity measures) and complexity measures in networks and matrices. The package includes:
11
+
12
+ # some functions (thus far)
13
+ - Box-counting fractal dimension calculation for networks and matrices
14
+ - Higuchi fractal dimension implementation
15
+ - Network-to-matrix and image-to-matrix conversions
16
+ - Dummy fractal generators for testing
@@ -0,0 +1,8 @@
1
+ # overview
2
+ computing fractal dimensions (and other complexity measures) and complexity measures in networks and matrices. The package includes:
3
+
4
+ # some functions (thus far)
5
+ - Box-counting fractal dimension calculation for networks and matrices
6
+ - Higuchi fractal dimension implementation
7
+ - Network-to-matrix and image-to-matrix conversions
8
+ - Dummy fractal generators for testing
File without changes
@@ -0,0 +1,4 @@
1
+ from .matrix import DummyAdjacencyMatrix
2
+ from .networks import WebFractalNetwork, SierpinskiFractal
3
+
4
+ __all__ = ["DummyAdjacencyMatrix", "WebFractalNetwork", "SierpinskiFractal"]
@@ -0,0 +1,150 @@
1
+ """
2
+ Fractal adjacency matrix generation and analysis with varying degrees of complexity
3
+ """
4
+
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+
8
+
9
+ class DummyAdjacencyMatrix:
10
+ def __init__(self, size, fractal_dimension):
11
+ """Initialize the FractalAdjacencyMatrix with a given size and fractal dimension.
12
+
13
+ Parameters:
14
+ size (int): The desired size of the adjacency matrix.
15
+ fractal_dimension (float): The target fractal dimension.
16
+ """
17
+ self.size, self.fractal_dimension = size, fractal_dimension
18
+
19
+ def generate_matrix(self, remove_upper_triangle=False, save_as=None):
20
+ """Generate the fractal adjacency matrix based on the specified fractal dimension.
21
+
22
+ Parameters:
23
+ fractal_dimension (float): The target fractal dimension.
24
+
25
+ Returns:
26
+ numpy.ndarray: The generated adjacency matrix.
27
+ """
28
+ max_m = min(self.size, 10)
29
+ m, pattern = self.calculate_m_and_pattern(self.fractal_dimension, max_m)
30
+ iterations = int(np.ceil(np.log(self.size) / np.log(m)))
31
+ actual_size = m**iterations
32
+ matrix = np.ones((actual_size, actual_size), dtype=bool)
33
+ self.apply_pattern(matrix, 0, 0, actual_size, m, pattern, iterations)
34
+
35
+ # Resize matrix if needed
36
+ if actual_size != self.size:
37
+ if actual_size > self.size:
38
+ matrix = matrix[: self.size, : self.size]
39
+ else:
40
+ pad_size = self.size - actual_size
41
+ matrix = np.pad(matrix, ((0, pad_size), (0, pad_size)), mode="constant", constant_values=False)
42
+
43
+ matrix = self.post_processing_matrix(matrix, remove_upper_triangle)
44
+
45
+ if save_as:
46
+ save_as = save_as if save_as.endswith(".png") else f"{save_as}.png"
47
+ plt.figure(figsize=(8, 8))
48
+ plt.imshow(matrix, cmap="binary", interpolation="nearest")
49
+ plt.title("Dummy Fractal Adjacency Matrix / FD ~ {:.2f}".format(self.fractal_dimension))
50
+ plt.axis("off")
51
+ plt.tight_layout()
52
+ plt.savefig(save_as, dpi=420, bbox_inches="tight")
53
+ plt.close()
54
+
55
+ return matrix
56
+
57
+ @staticmethod
58
+ def post_processing_matrix(matrix, remove_upper_triangle=False):
59
+ matrix = matrix.astype(int)
60
+ if remove_upper_triangle:
61
+ matrix = np.tril(matrix)
62
+ return matrix
63
+
64
+ def calculate_m_and_pattern(self, target_dimension, max_scaling_factor=10):
65
+ """Calculate the scaling factor 'm' and the base pattern for the fractal.
66
+
67
+ Parameters:
68
+ target_dimension (float): The target fractal dimension.
69
+ max_m (int): The maximum value of 'm' to consider (default is 10).
70
+
71
+ Returns:
72
+ tuple: A tuple containing 'm' and the 'pattern' numpy.ndarray.
73
+ """
74
+ best_dimension_diff = float("inf")
75
+ best_scaling_factor = None
76
+ best_pattern = None
77
+
78
+ for scaling_factor in range(2, max_scaling_factor + 1):
79
+ total_cells = scaling_factor * scaling_factor
80
+ # Iterate over possible numbers of cells to remove
81
+ for removed_cells in range(1, total_cells):
82
+ remaining_cells = total_cells - removed_cells
83
+ fractal_dim = np.log(remaining_cells) / np.log(scaling_factor)
84
+ dimension_diff = abs(fractal_dim - target_dimension)
85
+
86
+ if dimension_diff < best_dimension_diff:
87
+ best_dimension_diff = dimension_diff
88
+ best_scaling_factor = scaling_factor
89
+ # Create the base pattern
90
+ pattern = np.ones((scaling_factor, scaling_factor), dtype=bool)
91
+ # Determine which cells to remove, starting from the center
92
+ indices = [(i, j) for i in range(scaling_factor) for j in range(scaling_factor)]
93
+ center = scaling_factor // 2
94
+ indices_sorted = sorted(
95
+ indices,
96
+ key=lambda x: (abs(x[0] - center) + abs(x[1] - center), x[0], x[1]),
97
+ )
98
+ for i, j in indices_sorted[:removed_cells]:
99
+ pattern[i, j] = False
100
+ best_pattern = pattern.copy()
101
+
102
+ if best_dimension_diff == 0:
103
+ return best_scaling_factor, best_pattern
104
+
105
+ return best_scaling_factor, best_pattern
106
+
107
+ def apply_pattern(self, matrix, x_start, y_start, size, m, pattern, iterations_left):
108
+ """Recursively apply the fractal pattern to the matrix.
109
+
110
+ Parameters:
111
+ matrix (numpy.ndarray): The matrix to modify.
112
+ x_start (int): The starting x-coordinate in the matrix.
113
+ y_start (int): The starting y-coordinate in the matrix.
114
+ size (int): The size of the current square in the matrix.
115
+ m (int): The scaling factor.
116
+ pattern (numpy.ndarray): The base pattern to apply.
117
+ iterations_left (int): The number of iterations left to apply.
118
+ """
119
+ if iterations_left == 0:
120
+ return
121
+ step = size // m
122
+ for i, j in np.ndindex(m, m):
123
+ x_slice = slice(x_start + j * step, x_start + (j + 1) * step)
124
+ y_slice = slice(y_start + i * step, y_start + (i + 1) * step)
125
+ if not pattern[i, j]:
126
+ matrix[y_slice, x_slice] = False
127
+ else:
128
+ self.apply_pattern(
129
+ matrix,
130
+ x_start + j * step,
131
+ y_start + i * step,
132
+ step,
133
+ m,
134
+ pattern,
135
+ iterations_left - 1,
136
+ )
137
+
138
+
139
+ if __name__ == "__main__":
140
+ size = 256
141
+ fractal_dimensions = np.linspace(1.0, 2.0, 10)
142
+
143
+ for idx, dimension in enumerate(fractal_dimensions):
144
+ generator = DummyAdjacencyMatrix(size, dimension)
145
+ plt.figure(figsize=(6, 6))
146
+ plt.title(f"Fractal Dimension ~ {dimension:.2f}", pad=20)
147
+ plt.imshow(generator.matrix, cmap="binary", interpolation="nearest")
148
+ plt.axis("off")
149
+ plt.savefig(f"fractal_{idx}.png")
150
+ plt.close()
@@ -0,0 +1,111 @@
1
+ #!/Users/donyin/miniconda3/envs/rotation-1/bin/python
2
+
3
+ """
4
+ all the networkx class dummy implementations
5
+ """
6
+
7
+ import numpy as np
8
+ import networkx as nx
9
+
10
+
11
+ class WebFractalNetwork:
12
+ """
13
+ Generates a pseudo-fractal network using an iterative method.
14
+
15
+ Usage:
16
+ web_fractal = WebFractalNetwork(complexity=3)
17
+ network = web_fractal.network
18
+ """
19
+
20
+ def __init__(self, complexity=3):
21
+ self.complexity = complexity
22
+ self.network = self._generate_network()
23
+ self.diameter = nx.diameter(self.network)
24
+
25
+ def _generate_network(self):
26
+ """
27
+ Generates the pseudo-fractal network.
28
+ """
29
+ G = nx.Graph()
30
+ G.add_edges_from([(0, 1), (1, 2), (2, 0)])
31
+ for _ in range(self.complexity):
32
+ G = self._fractal_step(G)
33
+ return G
34
+
35
+ def _fractal_step(self, G):
36
+ """
37
+ Performs one iteration of the fractal construction.
38
+ """
39
+ new_edges = []
40
+ max_node = max(G.nodes())
41
+ for u, v in G.edges():
42
+ new_node = max_node + 1
43
+ max_node += 1
44
+ new_edges.extend([(new_node, u), (new_node, v)])
45
+ G.add_edges_from(new_edges)
46
+ return G
47
+
48
+
49
+ class SierpinskiFractal:
50
+ """
51
+ Generates a Sierpinski gasket fractal network.
52
+
53
+ Usage:
54
+ sierpinski = SierpinskiFractal(recursion_depth=3)
55
+ network = sierpinski.network
56
+ """
57
+
58
+ def __init__(self, recursion_depth=3):
59
+ self.recursion_depth = recursion_depth
60
+ self.points = {}
61
+ self.edges = set()
62
+ self.point_index = 0
63
+ self.network = self._generate_network()
64
+
65
+ def _generate_network(self):
66
+ """
67
+ Generates the Sierpinski gasket fractal network.
68
+ """
69
+ vertices = [(0, 0), (0.5, np.sqrt(3) / 2), (1, 0)]
70
+ self._sierpinski_recursive(vertices, self.recursion_depth)
71
+ return self._build_graph()
72
+
73
+ def _sierpinski_recursive(self, vertices, depth):
74
+ """
75
+ Recursively generates the Sierpinski gasket fractal.
76
+ """
77
+
78
+ def add_triangle(v1, v2, v3, current_depth):
79
+ if current_depth == 0:
80
+ for v in [v1, v2, v3]:
81
+ if v not in self.points:
82
+ self.points[v] = self.point_index
83
+ self.point_index += 1
84
+ i1, i2, i3 = self.points[v1], self.points[v2], self.points[v3]
85
+ self.edges.update([(i1, i2), (i2, i3), (i3, i1)])
86
+ else:
87
+ m1 = ((v1[0] + v2[0]) / 2, (v1[1] + v2[1]) / 2)
88
+ m2 = ((v2[0] + v3[0]) / 2, (v2[1] + v3[1]) / 2)
89
+ m3 = ((v3[0] + v1[0]) / 2, (v3[1] + v1[1]) / 2)
90
+ add_triangle(v1, m1, m3, current_depth - 1)
91
+ add_triangle(m1, v2, m2, current_depth - 1)
92
+ add_triangle(m3, m2, v3, current_depth - 1)
93
+
94
+ add_triangle(vertices[0], vertices[1], vertices[2], depth)
95
+
96
+ def _build_graph(self):
97
+ """
98
+ Builds the NetworkX graph from the generated points and edges.
99
+ """
100
+ G = nx.Graph()
101
+ for coord, idx in self.points.items():
102
+ G.add_node(idx, pos=coord)
103
+ G.add_edges_from(self.edges)
104
+ return G
105
+
106
+
107
+ if __name__ == "__main__":
108
+ network = SierpinskiFractal(recursion_depth=7)
109
+ # network = WebFractalNetwork(complexity=4)
110
+ # handler = FractalHandlerNetworkX(network.network)
111
+ # handler.compute_fractal_dimensions(box_sizes="auto", iterations=10)
@@ -0,0 +1,5 @@
1
+ from .higuchi import hfd_matlab_equivalent, hfd_pyeeg
2
+ from .cbb_matrix import FractalHandlerMatrix
3
+ from .cbb_networkx import FractalHandlerNetworkX
4
+
5
+ __all__ = ["hfd_matlab_equivalent", "hfd_pyeeg", "FractalHandlerMatrix", "FractalHandlerNetworkX"]
@@ -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)
@@ -0,0 +1,100 @@
1
+ #!/Users/donyin/miniconda3/envs/rotation-1/bin/python
2
+
3
+ """
4
+ - now we have two 1d series as signals make dummy data using numpy
5
+ - use either a sliding window or regression method to produce a new signal that represents the co-activation between the two signals
6
+ - in the sliding_window method, the window_size parameter is used to determine the size of the window used to calculate the co-activation
7
+ - it moves one step at a time and calculates the correlation between the two signals in the window
8
+ - in the regression method, the two signals are used as independent and dependent variables in a linear regression model
9
+ - the resulting signal is the predicted dependent variable
10
+ - if we use the sliding_window method, the window_size parameter must be an integer greater than 0
11
+ - if we use the regression method, we must plot the two signal and the resulting signal
12
+ """
13
+
14
+ import numpy as np
15
+ from sklearn.linear_model import LinearRegression
16
+ import matplotlib.pyplot as plt
17
+
18
+
19
+ def get_co_activation(
20
+ signal_1: np.ndarray, signal_2: np.ndarray, method: str, window_size: int = None, save_plot_as=None
21
+ ) -> np.ndarray:
22
+ """
23
+ Get a measure of co-activation between two 1D signals.
24
+
25
+ Args:
26
+ signal_1: First 1D time series signal
27
+ signal_2: Second 1D time series signal
28
+ method: Either 'sliding_window' or 'regression'
29
+ window_size: Size of the window for 'sliding_window' method (must be an integer greater than 0)
30
+
31
+ Returns:
32
+ np.ndarray: A new signal representing the co-activation between the two signals
33
+ """
34
+ match method:
35
+ case "sliding_window":
36
+ if window_size is None or window_size <= 0:
37
+ raise ValueError("window_size must be an integer greater than 0 for sliding_window method")
38
+ correlations = []
39
+ num_windows = len(signal_1) - window_size + 1
40
+ for i in range(num_windows):
41
+ window_signal_1 = signal_1[i : i + window_size]
42
+ window_signal_2 = signal_2[i : i + window_size]
43
+ corr = np.corrcoef(window_signal_1, window_signal_2)[0, 1]
44
+ correlations.append(corr)
45
+
46
+ # Plot signals and correlations
47
+ correlations = np.array(correlations)
48
+ if save_plot_as:
49
+ plt.figure(figsize=(10, 5))
50
+ plt.plot(signal_1, label="Signal 1", alpha=0.7)
51
+ plt.plot(signal_2, label="Signal 2", alpha=0.7)
52
+ plt.plot(
53
+ np.arange(window_size // 2, len(correlations) + window_size // 2),
54
+ correlations,
55
+ label="Sliding Window Correlation",
56
+ linestyle="--",
57
+ )
58
+ plt.legend()
59
+ plt.title(f"Sliding Window Method Co-Activation (window size={window_size})")
60
+ plt.savefig(save_plot_as, dpi=320)
61
+ plt.close()
62
+
63
+ return correlations
64
+
65
+ case "regression":
66
+ # Reshape signals for regression
67
+ X = signal_1.reshape(-1, 1)
68
+ y = signal_2
69
+
70
+ # Fit linear regression
71
+ model = LinearRegression()
72
+ model.fit(X, y)
73
+
74
+ # Predict y
75
+ y_pred = model.predict(X)
76
+
77
+ # Plot signals
78
+ if save_plot_as:
79
+ plt.figure(figsize=(10, 5))
80
+ plt.plot(signal_1, label="Signal 1 (IV)")
81
+ plt.plot(signal_2, label="Signal 2 (DV)")
82
+ plt.plot(y_pred, label="Predicted DV", linestyle="--")
83
+ plt.legend()
84
+ plt.title("Regression Method Co-Activation")
85
+ plt.savefig(save_plot_as, dpi=320)
86
+ plt.close()
87
+
88
+ return y_pred
89
+
90
+ case _:
91
+ raise ValueError("method must be 'sliding_window' or 'regression'")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ t = np.linspace(0, 10, 1000)
96
+ signal1 = np.sin(2 * np.pi * 0.5 * t) + 0.2 * np.random.randn(1000)
97
+ signal2 = 0.8 * np.sin(2 * np.pi * 0.5 * t + np.pi / 4) + 0.2 * np.random.randn(1000)
98
+
99
+ get_co_activation(signal1, signal2, method="sliding_window", window_size=32, save_plot_as="coactivation_sliding_window.png")
100
+ get_co_activation(signal1, signal2, method="regression", save_plot_as="coactivation_regression.png")
@@ -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):
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):
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):
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):
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,96 @@
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 complexeva.measures.higuchi import hfd_pyeeg, hfd_matlab_equivalent
9
+
10
+
11
+ class HiguchiMethodValidate:
12
+ def __init__(self, funcs, k_max: int, timeseries_len=1000, num_hurst=200):
13
+ self.funcs, self.k_max, self.num_hurst, self.timeseries_len = funcs, k_max, num_hurst, timeseries_len
14
+
15
+ def fbm(self, hurst):
16
+ """
17
+ Generate fractional Brownian motion (fBm) time series.
18
+
19
+ This method uses the spectral synthesis method to generate a fBm time series
20
+ with a specified Hurst exponent.
21
+
22
+ Parameters:
23
+ -----------
24
+ hurst : float
25
+ The Hurst exponent, a value between 0 and 1 that characterizes
26
+ the fractal properties of the time series.
27
+
28
+ Returns:
29
+ --------
30
+ numpy.ndarray
31
+ A 1D array representing the generated fBm time series.
32
+
33
+ Notes:
34
+ ------
35
+ - The length of the generated time series is determined by self.timeseries_len.
36
+ - This method uses the Fourier filtering method to generate fBm.
37
+ - The first frequency component is set to a small non-zero value (1e-6) to avoid division by zero.
38
+ """
39
+
40
+ n = self.timeseries_len
41
+ f = numpy.fft.fftfreq(n)
42
+ f[0] = 1e-6 # avoid division by zero
43
+ psd = numpy.abs(f) ** (-2 * hurst - 1)
44
+ gaussian = numpy.random.randn(n) + 1j * numpy.random.randn(n)
45
+ fBm = numpy.fft.ifft(gaussian * numpy.sqrt(psd)).real
46
+ return fBm
47
+
48
+ def plot_hfd_vs_hurst(self):
49
+ hurst_exponent = numpy.linspace(0.001, 1, self.num_hurst)
50
+ fBm_series = [self.fbm(h) for h in hurst_exponent]
51
+ expected_hfd = [2 - h for h in hurst_exponent]
52
+ hfd_results = []
53
+
54
+ for func in self.funcs:
55
+ hfd_results.append([func(series, self.k_max) for series in fBm_series])
56
+
57
+ # ---- plotting ----
58
+ fig, axs = plt.subplots(3, 1, figsize=(9, 10))
59
+
60
+ axs[0].plot(fBm_series[0])
61
+ axs[0].set_title(f"fBm series with H = {hurst_exponent[0]:.3f}", fontsize=21)
62
+ mid_index = len(hurst_exponent) // 2
63
+ axs[1].plot(fBm_series[mid_index])
64
+ axs[1].set_title(f"fBm series with H = {hurst_exponent[mid_index]:.3f}", fontsize=21)
65
+ axs[2].plot(fBm_series[-1])
66
+ axs[2].set_title(f"fBm series with H = {hurst_exponent[-1]:.3f}", fontsize=21)
67
+
68
+ for ax in axs:
69
+ ax.set_xlabel("Time", fontsize=21)
70
+ ax.set_ylabel("Value", fontsize=21)
71
+ ax.tick_params(axis="both", which="major", labelsize=12)
72
+ ax.grid(True)
73
+
74
+ plt.tight_layout()
75
+ plt.savefig("fBm_series_examples.png", dpi=320, bbox_inches="tight")
76
+
77
+ # Original HFD vs Hurst plot
78
+ plt.figure(figsize=(9, 7))
79
+ plt.plot(hurst_exponent, expected_hfd, label="Expected HFD")
80
+ for i, func in enumerate(self.funcs):
81
+ plt.plot(hurst_exponent, hfd_results[i], label=f"HFD ({func.__name__})")
82
+ plt.xlabel("Hurst Exponent (H)", fontsize=14)
83
+ plt.ylabel("Higuchi Fractal Dimension (HFD)", fontsize=14)
84
+ plt.title(f"HFD vs Hurst Exponent ({len(hurst_exponent)} points)", fontsize=16)
85
+ plt.tight_layout()
86
+ plt.grid(True)
87
+ plt.xticks(fontsize=21)
88
+ plt.yticks(fontsize=21)
89
+ plt.legend(fontsize=21, bbox_to_anchor=(1.05, 1), loc="upper left")
90
+ plt.savefig("hfd_vs_hurst.png", dpi=320, bbox_inches="tight")
91
+
92
+
93
+ if __name__ == "__main__":
94
+ k_max = 30
95
+ validator = HiguchiMethodValidate([hfd_pyeeg, hfd_matlab_equivalent], k_max)
96
+ validator.plot_hfd_vs_hurst()
@@ -0,0 +1,2 @@
1
+ from .convert_image_matrix import image_to_adjacency_matrix
2
+ from .convert_matrix_network import matrix_to_networkx
@@ -0,0 +1,48 @@
1
+ #!/Users/donyin/miniconda3/envs/rotation-1/bin/python
2
+
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+
6
+ import numpy as np
7
+ from skimage.io import imread
8
+ from skimage.color import rgb2gray
9
+ from skimage.filters import threshold_otsu
10
+ from skimage.morphology import remove_small_objects, remove_small_holes
11
+
12
+
13
+ def image_to_adjacency_matrix(image_path, threshold=128, hole_area_threshold_ratio=0.05, save_as=None):
14
+ """
15
+ Convert an image to a binary adjacency matrix using scikit-image preprocessing.
16
+ - save_as: The path to save the processed binary image.
17
+ - threshold: The threshold value for binarization. max value is 255.
18
+ - hole_area_threshold_ratio: The ratio of the hole area to the image area to be removed (ratio to the diagnal length).
19
+ """
20
+ image = imread(image_path)
21
+ if image.ndim == 3:
22
+ if image.shape[2] == 4:
23
+ image = image[..., :3]
24
+ image = rgb2gray(image)
25
+
26
+ threshold = threshold_otsu(image)
27
+ binary_image = image > threshold
28
+
29
+ # ---- some clean up ----
30
+ # binary_image = remove_small_objects(binary_image, min_size=64)
31
+ area_threshold = hole_area_threshold_ratio * np.hypot(*binary_image.shape)
32
+ binary_image = remove_small_holes(binary_image, area_threshold=area_threshold)
33
+ binary_matrix = binary_image.astype(np.uint8)
34
+
35
+ if save_as:
36
+ plt.figure(figsize=(16, 8))
37
+ plt.subplot(1, 2, 1)
38
+ plt.imshow(imread(image_path), interpolation="nearest")
39
+ plt.title("Original Image")
40
+ plt.axis("off")
41
+ plt.subplot(1, 2, 2)
42
+ plt.imshow(binary_matrix, cmap="binary", interpolation="nearest")
43
+ plt.title("Processed Binary Image")
44
+ plt.axis("off")
45
+ plt.tight_layout()
46
+ plt.savefig(save_as, dpi=300, bbox_inches="tight")
47
+ plt.close()
48
+ return binary_matrix
@@ -0,0 +1,71 @@
1
+ import networkx as nx
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from rich import print
5
+
6
+
7
+ def matrix_to_networkx(matrix, method: str = "laplacian", save_as=None):
8
+ """
9
+ Convert a symmetric adjacency matrix to a NetworkX graph.
10
+ Parameters: matrix (numpy.ndarray): A 2D numpy array representing the adjacency matrix.
11
+ Returns: networkx.Graph: A NetworkX graph object.
12
+ """
13
+
14
+ assert method in ["adjacency", "laplacian"], "> method should be either 'adjacency' or 'laplacian'"
15
+
16
+ match method:
17
+ case "adjacency":
18
+ """
19
+ convert a binary matrix representing a 2D fractal image into a NetworkX graph.
20
+ - nodes are pixels with value 1, and edges connect neighboring pixels.
21
+ - check neighbors (4-connectivity)
22
+ """
23
+ G = nx.Graph()
24
+ rows, cols = matrix.shape
25
+ for i in range(rows):
26
+ for j in range(cols):
27
+ if matrix[i, j]:
28
+ G.add_node((i, j))
29
+ for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
30
+ ni, nj = i + dx, j + dy
31
+ if 0 <= ni < rows and 0 <= nj < cols and matrix[ni, nj]:
32
+ G.add_edge((i, j), (ni, nj))
33
+
34
+ case "laplacian":
35
+ if matrix.shape[0] != matrix.shape[1]:
36
+ previous_size = matrix.shape
37
+ matrix = np.pad(matrix, ((0, max(matrix.shape) - matrix.shape[0]), (0, max(matrix.shape) - matrix.shape[1])))
38
+ print(f"> warning: Laplacian matrix should be square. padded matrix: {previous_size} -> {matrix.shape}")
39
+ G = nx.from_numpy_array(matrix)
40
+ G.remove_nodes_from(list(nx.isolates(G)))
41
+
42
+ if save_as:
43
+ save_as = save_as if save_as.endswith(".png") else save_as + ".png"
44
+ fig = plt.figure(figsize=(12, 5))
45
+
46
+ # Plot matrix in first subplot
47
+ ax1 = fig.add_subplot(121)
48
+ ax1.imshow(matrix, cmap="viridis")
49
+ ax1.set_title("Original Matrix")
50
+ ax1.axis("off")
51
+
52
+ # Plot 3D network graph
53
+ ax2 = fig.add_subplot(122, projection="3d")
54
+ pos = nx.spring_layout(G, dim=3)
55
+
56
+ # Draw edges
57
+ edge_pos = np.array([(pos[u], pos[v]) for u, v in G.edges()])
58
+ for edge in edge_pos:
59
+ ax2.plot3D(edge[:, 0], edge[:, 1], edge[:, 2], "gray", alpha=0.6)
60
+
61
+ # Draw nodes
62
+ node_pos = np.array([pos[node] for node in G.nodes()])
63
+ ax2.scatter(node_pos[:, 0], node_pos[:, 1], node_pos[:, 2], c="blue", s=20, alpha=0.6)
64
+
65
+ ax2.set_title("3D Network Graph")
66
+
67
+ plt.tight_layout()
68
+ plt.savefig(save_as, dpi=300, bbox_inches="tight")
69
+ plt.close()
70
+
71
+ return G
@@ -0,0 +1,29 @@
1
+ import torch
2
+
3
+
4
+ def select_device(priority=["cuda", "cpu"]): # mps in the middle
5
+ """
6
+ Selects the device based on the given priority list.
7
+ If top priority device is not available, it will try the next device in the list.
8
+ E.g., it will try to select "cuda" first, then "mps", and finally "cpu"
9
+ So we can have the same code on local machine and on the server.
10
+ Just with
11
+
12
+ - local: select_device(["cuda", "cpu"])
13
+ - server: select_device(["cuda", "cpu"])
14
+
15
+ Parameters:
16
+ - priority (list): List of strings representing device priorities.
17
+
18
+ Returns:
19
+ - torch.device: Device selected based on priority.
20
+ """
21
+
22
+ if "cuda" in priority and torch.cuda.is_available():
23
+ return torch.device("cuda")
24
+ if "mps" in priority and torch.backends.mps.is_available() and torch.backends.mps.is_built():
25
+ return torch.device("mps")
26
+ if "cpu" in priority:
27
+ return torch.device("cpu")
28
+
29
+ raise ValueError("No valid device found in priority list.")
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: complexeva
3
+ Version: 0.1.5
4
+ Home-page:
5
+ Author: Don Yin
6
+ Author-email: don_yin@outlook.com
7
+ Description-Content-Type: text/markdown
8
+
9
+ # overview
10
+ computing fractal dimensions (and other complexity measures) and complexity measures in networks and matrices. The package includes:
11
+
12
+ # some functions (thus far)
13
+ - Box-counting fractal dimension calculation for networks and matrices
14
+ - Higuchi fractal dimension implementation
15
+ - Network-to-matrix and image-to-matrix conversions
16
+ - Dummy fractal generators for testing
@@ -0,0 +1,20 @@
1
+ README.md
2
+ setup.py
3
+ complexeva/__init__.py
4
+ complexeva.egg-info/PKG-INFO
5
+ complexeva.egg-info/SOURCES.txt
6
+ complexeva.egg-info/dependency_links.txt
7
+ complexeva.egg-info/top_level.txt
8
+ complexeva/dummies/__init__.py
9
+ complexeva/dummies/matrix.py
10
+ complexeva/dummies/networks.py
11
+ complexeva/measures/__init__.py
12
+ complexeva/measures/cbb_matrix.py
13
+ complexeva/measures/cbb_networkx.py
14
+ complexeva/measures/co_activation.py
15
+ complexeva/measures/higuchi.py
16
+ complexeva/measures/validate.py
17
+ complexeva/utils/__init__.py
18
+ complexeva/utils/convert_image_matrix.py
19
+ complexeva/utils/convert_matrix_network.py
20
+ complexeva/utils/device.py
@@ -0,0 +1 @@
1
+ complexeva
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="complexeva",
5
+ version="0.1.5",
6
+ packages=find_packages(),
7
+ install_requires=[],
8
+ author="Don Yin",
9
+ author_email="don_yin@outlook.com",
10
+ description="",
11
+ long_description=open("README.md").read(),
12
+ long_description_content_type="text/markdown",
13
+ url="",
14
+ pyhon_requires=">=3.11",
15
+ )