pyafv 0.3.8__cp314-cp314t-macosx_10_15_x86_64.whl

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.
pyafv/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """
2
+ PyAFV - A Python implementation of the **active-finite-Voronoi (AFV) model** in 2D.
3
+ """
4
+
5
+ from .physical_params import PhysicalParams, target_delta
6
+ from .finite_voronoi import FiniteVoronoiSimulator
7
+
8
+ try:
9
+ from ._version import __version__
10
+ except ImportError: # pragma: no cover
11
+ __version__ = "unknown"
12
+
13
+
14
+ __all__ = [
15
+ "PhysicalParams",
16
+ "FiniteVoronoiSimulator",
17
+ "target_delta",
18
+ ]
pyafv/_version.py ADDED
@@ -0,0 +1,2 @@
1
+ # pyafv/_version.py
2
+ __version__ = "0.3.8"
pyafv/backend.py ADDED
@@ -0,0 +1,20 @@
1
+ # chooses fast vs fallback implementation
2
+
3
+ try:
4
+ from . import cell_geom as _impl
5
+ _BACKEND_NAME = "cython"
6
+ _IMPORT_ERROR = None
7
+ except Exception as e: # pragma: no cover
8
+ from . import cell_geom_fallback as _impl
9
+ _BACKEND_NAME = "python"
10
+ _IMPORT_ERROR = e
11
+
12
+
13
+ # ---- for explicit API ----
14
+ backend_impl = _impl
15
+
16
+ __all__ = [
17
+ "backend_impl",
18
+ "_BACKEND_NAME",
19
+ "_IMPORT_ERROR"
20
+ ]
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,249 @@
1
+ """
2
+ These are pure-Python fallback implementations of key functions for finite Voronoi
3
+ cell geometry and derivative calculations, used when Cython extensions are not
4
+ available.
5
+ """
6
+
7
+ import numpy as np
8
+ from collections import defaultdict
9
+
10
+
11
+ def build_vertexpair_and_vertexpoints(ridge_vertices_all, ridge_points, num_vertices, N):
12
+ # Build ridge incidence per vertex, and a lookup for (v1,v2) -> ridge id
13
+ vertex_incident_ridges = defaultdict(list)
14
+ # Cheaper key: store both directions as tuple -> int
15
+ vertexpair2ridge: dict[tuple[int, int], int] = {}
16
+
17
+ rv_full = np.asarray(ridge_vertices_all, dtype=int)
18
+ R = rv_full.shape[0]
19
+ for k in range(R):
20
+ v1, v2 = int(rv_full[k, 0]), int(rv_full[k, 1])
21
+ vertex_incident_ridges[v1].append(k)
22
+ vertex_incident_ridges[v2].append(k)
23
+ vertexpair2ridge[(v1, v2)] = k
24
+ vertexpair2ridge[(v2, v1)] = k
25
+
26
+ # For each finite vertex, record which input points (cells) meet there
27
+ vertex_points = {}
28
+ if N > 2:
29
+ for v_id in range(num_vertices):
30
+ s = set()
31
+ for ridge_id in vertex_incident_ridges[v_id]:
32
+ i, j = ridge_points[ridge_id]
33
+ s.add(i), s.add(j)
34
+ vertex_points[v_id] = list(s)
35
+
36
+ return vertexpair2ridge, vertex_points
37
+
38
+
39
+ def pad_regions(regions):
40
+ # fake function to match cython backend interface
41
+ return regions
42
+
43
+
44
+ def build_point_edges(vor_regions, point_region,
45
+ vertices_all, pts,
46
+ num_vertices, vertexpair2ridge,
47
+ p1, p1_edges_pack, p1_verts_pack,
48
+ p2, p2_edges_pack, p2_verts_pack):
49
+
50
+ N = pts.shape[0]
51
+
52
+ point_edges_type = []
53
+ point_vertices_f_idx = []
54
+
55
+ # --- fast vectorized per-cell processing (no inner edge loop) ---
56
+ for idx in range(N):
57
+ region_id = point_region[idx]
58
+ v_ids = np.asarray(vor_regions[region_id], dtype=int)
59
+
60
+ if v_ids.size == 0:
61
+ point_edges_type.append([])
62
+ point_vertices_f_idx.append([])
63
+ continue
64
+
65
+ # sort vertices clockwise around cell center
66
+ rel = vertices_all[v_ids] - pts[idx]
67
+ angles = np.arctan2(rel[:, 1], rel[:, 0])
68
+ order = np.argsort(angles)[::-1]
69
+ v_ids = v_ids[order]
70
+
71
+ # consecutive pairs (wrap) -> candidate edges around this cell
72
+ v1_ids = v_ids
73
+ v2_ids = np.roll(v_ids, -1)
74
+
75
+ # skip ray-ray (both >= num_vertices)
76
+ valid = ~((v1_ids >= num_vertices) & (v2_ids >= num_vertices))
77
+ if not np.any(valid): # pragma: no cover
78
+ point_edges_type.append([])
79
+ point_vertices_f_idx.append([])
80
+ continue
81
+
82
+ v1_ids = v1_ids[valid]
83
+ v2_ids = v2_ids[valid]
84
+
85
+ # ---- vectorized ridge id lookup for all edges of this cell ----
86
+ # use the dict you already built with both orientations:
87
+ # vertexpair2ridge[(v1, v2)] = ridge_id
88
+ # convert all edge pairs in one go via list comprehension (still fast, no Python loop per-edge body)
89
+ # NB: we keep it simple & reliable; if needed, switch to a sorted-structured-array map later.
90
+ keys = list(zip(v1_ids.tolist(), v2_ids.tolist()))
91
+ ridge_ids = np.fromiter(
92
+ (vertexpair2ridge[k] for k in keys), dtype=int, count=len(keys))
93
+
94
+ # decide which endpoint pack to use (p1 vs p2) for each edge
95
+ use_p1 = (p1[ridge_ids] == idx)
96
+ use_p2 = ~use_p1
97
+
98
+ # gather packs (shape (E,3)), then mask out the -1 slots
99
+ pack_e = np.empty((len(ridge_ids), 3), dtype=int)
100
+ pack_v = np.empty((len(ridge_ids), 3), dtype=int)
101
+
102
+ if np.any(use_p1):
103
+ pack_e[use_p1] = p1_edges_pack[ridge_ids[use_p1]]
104
+ pack_v[use_p1] = p1_verts_pack[ridge_ids[use_p1]]
105
+ if np.any(use_p2):
106
+ pack_e[use_p2] = p2_edges_pack[ridge_ids[use_p2]]
107
+ pack_v[use_p2] = p2_verts_pack[ridge_ids[use_p2]]
108
+
109
+ # flatten valid entries in pack order (keeps your exact edge ordering)
110
+ mask = (pack_e >= 0)
111
+ edges_type = pack_e[mask].tolist()
112
+ vertices_f_idx = pack_v[mask].tolist()
113
+
114
+ if len(vertices_f_idx) != len(edges_type):
115
+ raise ValueError("Vertex and edge number not equal!")
116
+
117
+ point_edges_type.append(edges_type)
118
+ point_vertices_f_idx.append(vertices_f_idx)
119
+
120
+ return point_edges_type, point_vertices_f_idx
121
+
122
+
123
+ def compute_vertex_derivatives(
124
+ point_edges_type, # list-of-lists / arrays of edge types
125
+ point_vertices_f_idx, # list-of-lists / arrays of vertex ids
126
+ vertices_all,
127
+ pts, r, A0_list, P0, num_vertices_ext, num_ridges,
128
+ vertex_out_points):
129
+
130
+ N = pts.shape[0]
131
+
132
+ # --- helpers ---
133
+ def _row_cross(a, b):
134
+ # z-component of 2D cross, row-wise
135
+ return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]
136
+
137
+ def _perp(u):
138
+ # rotate 90° CW: (ux,uy) -> (uy,-ux)
139
+ return np.column_stack((u[:, 1], -u[:, 0]))
140
+
141
+ vertex_out_da_dtheta = np.zeros((2 * num_ridges, 2))
142
+ vertex_out_dl_dtheta = np.zeros((2 * num_ridges, 2))
143
+
144
+ dA_poly_dh = np.zeros((num_vertices_ext + 2 * num_ridges, 2))
145
+ dP_poly_dh = np.zeros((num_vertices_ext + 2 * num_ridges, 2))
146
+
147
+ area_list = np.zeros(N)
148
+ perimeter_list = np.zeros(N)
149
+
150
+ for idx in range(N):
151
+ edges_type = np.asarray(point_edges_type[idx], dtype=int)
152
+ vertices_f_idx = np.asarray(point_vertices_f_idx[idx], dtype=int)
153
+ E = edges_type.size
154
+
155
+ if E < 2:
156
+ area_list[idx] = np.pi * (r ** 2)
157
+ perimeter_list[idx] = 2.0 * np.pi * r
158
+ continue
159
+
160
+ # ring indices
161
+ v1_idx = vertices_f_idx
162
+ v2_idx = np.roll(vertices_f_idx, -1)
163
+ v0_idx = np.roll(vertices_f_idx, 1)
164
+
165
+ ri = pts[idx]
166
+ V1 = vertices_all[v1_idx]
167
+ V2 = vertices_all[v2_idx]
168
+ V0 = vertices_all[v0_idx]
169
+ V1mR = V1 - ri
170
+ V2mR = V2 - ri
171
+ V0mR = V0 - ri
172
+
173
+ mask_str = (edges_type == 1)
174
+ mask_arc = ~mask_str
175
+
176
+ # ----- perimeter & area -----
177
+ seg12 = V1 - V2
178
+ l12 = np.linalg.norm(seg12, axis=1)
179
+ Pi_straight = l12[mask_str].sum()
180
+ Ai_straight = (-0.5 * _row_cross(V1mR[mask_str], V2mR[mask_str])).sum()
181
+
182
+ if np.any(mask_arc):
183
+ a1_full = np.arctan2(V1mR[:, 1], V1mR[:, 0])
184
+ a2_full = np.arctan2(V2mR[:, 1], V2mR[:, 0])
185
+ dangle_full = (a1_full - a2_full) % (2.0 * np.pi)
186
+ dangle_arc = dangle_full[mask_arc]
187
+ Pi_arc = (r * dangle_arc).sum()
188
+ Ai_arc = (0.5 * (r ** 2) * dangle_arc).sum()
189
+ else:
190
+ Pi_arc = 0.0
191
+ Ai_arc = 0.0
192
+
193
+ Pi = Pi_straight + Pi_arc
194
+ Ai = Ai_straight + Ai_arc
195
+ perimeter_list[idx] = Pi
196
+ area_list[idx] = Ai
197
+
198
+ # ----- dA_poly/dh, dP_poly/dh for v1 -----
199
+ dAi_v1 = -0.5 * _perp(V2mR) + 0.5 * _perp(V0mR) # (E,2)
200
+
201
+ dPi_v1 = np.zeros((E, 2))
202
+ if np.any(mask_str):
203
+ dPi_v1[mask_str] += seg12[mask_str] / l12[mask_str][:, None]
204
+
205
+ mask_prev_str = np.roll(mask_str, 1)
206
+ seg10 = V1 - V0
207
+ l10 = np.linalg.norm(seg10, axis=1)
208
+ if np.any(mask_prev_str):
209
+ dPi_v1[mask_prev_str] += seg10[mask_prev_str] / l10[mask_prev_str][:, None]
210
+
211
+ np.add.at(dA_poly_dh, v1_idx, (Ai - A0_list[idx]) * dAi_v1)
212
+ np.add.at(dP_poly_dh, v1_idx, (Pi - P0) * dPi_v1)
213
+
214
+ # ----- arc endpoint sensitivities at outer vertices -----
215
+ if np.any(mask_arc):
216
+ # endpoint rows in vertex_out_* are (outer_id - num_vertices_ext)
217
+ v1_arc_idx = v1_idx[mask_arc]
218
+ v2_arc_idx = v2_idx[mask_arc]
219
+ k1 = v1_arc_idx - num_vertices_ext
220
+ k2 = v2_arc_idx - num_vertices_ext
221
+ valid1 = (k1 >= 0)
222
+ valid2 = (k2 >= 0)
223
+
224
+ if np.any(valid1) or np.any(valid2):
225
+ # da/dtheta for endpoints (sector - triangle)
226
+ da1_full = 0.5 * (r ** 2) * (1.0 - np.cos(dangle_full)) # v1 endpoint
227
+ da2_full = -da1_full # v2 endpoint
228
+ da1_arc = da1_full[mask_arc]
229
+ da2_arc = da2_full[mask_arc]
230
+
231
+ # dl/dtheta is ±r
232
+ dl1 = r
233
+ dl2 = -r
234
+
235
+ vop = vertex_out_points # rows are sorted [i,j]; column 1 is max(i,j)
236
+ if np.any(valid1):
237
+ k1v = k1[valid1]
238
+ # CORRECT which_point: 0 if max(i,j) > idx else 1
239
+ which1 = (vop[k1v, 1] <= idx).astype(int)
240
+ vertex_out_da_dtheta[k1v, which1] = da1_arc[valid1]
241
+ vertex_out_dl_dtheta[k1v, which1] = dl1
242
+
243
+ if np.any(valid2):
244
+ k2v = k2[valid2]
245
+ which2 = (vop[k2v, 1] <= idx).astype(int)
246
+ vertex_out_da_dtheta[k2v, which2] = da2_arc[valid2]
247
+ vertex_out_dl_dtheta[k2v, which2] = dl2
248
+
249
+ return vertex_out_da_dtheta, vertex_out_dl_dtheta, dA_poly_dh, dP_poly_dh, area_list, perimeter_list