pyafv 0.3.6__cp314-cp314-musllinux_1_2_aarch64.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 +18 -0
- pyafv/_version.py +2 -0
- pyafv/backend.py +20 -0
- pyafv/cell_geom.cpython-314-aarch64-linux-musl.so +0 -0
- pyafv/cell_geom_fallback.py +249 -0
- pyafv/finite_voronoi.py +985 -0
- pyafv/physical_params.py +157 -0
- pyafv-0.3.6.dist-info/METADATA +118 -0
- pyafv-0.3.6.dist-info/RECORD +12 -0
- pyafv-0.3.6.dist-info/WHEEL +5 -0
- pyafv-0.3.6.dist-info/licenses/LICENSE +21 -0
- pyafv-0.3.6.dist-info/top_level.txt +1 -0
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
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
|
|
@@ -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
|