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.
- complexeva-0.1.5/PKG-INFO +16 -0
- complexeva-0.1.5/README.md +8 -0
- complexeva-0.1.5/complexeva/__init__.py +0 -0
- complexeva-0.1.5/complexeva/dummies/__init__.py +4 -0
- complexeva-0.1.5/complexeva/dummies/matrix.py +150 -0
- complexeva-0.1.5/complexeva/dummies/networks.py +111 -0
- complexeva-0.1.5/complexeva/measures/__init__.py +5 -0
- complexeva-0.1.5/complexeva/measures/cbb_matrix.py +134 -0
- complexeva-0.1.5/complexeva/measures/cbb_networkx.py +288 -0
- complexeva-0.1.5/complexeva/measures/co_activation.py +100 -0
- complexeva-0.1.5/complexeva/measures/higuchi.py +91 -0
- complexeva-0.1.5/complexeva/measures/validate.py +96 -0
- complexeva-0.1.5/complexeva/utils/__init__.py +2 -0
- complexeva-0.1.5/complexeva/utils/convert_image_matrix.py +48 -0
- complexeva-0.1.5/complexeva/utils/convert_matrix_network.py +71 -0
- complexeva-0.1.5/complexeva/utils/device.py +29 -0
- complexeva-0.1.5/complexeva.egg-info/PKG-INFO +16 -0
- complexeva-0.1.5/complexeva.egg-info/SOURCES.txt +20 -0
- complexeva-0.1.5/complexeva.egg-info/dependency_links.txt +1 -0
- complexeva-0.1.5/complexeva.egg-info/top_level.txt +1 -0
- complexeva-0.1.5/setup.cfg +4 -0
- complexeva-0.1.5/setup.py +15 -0
|
@@ -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,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,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,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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
complexeva
|
|
@@ -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
|
+
)
|