pyafv 0.3.3__cp312-cp312-musllinux_1_2_x86_64.whl → 0.3.4__cp312-cp312-musllinux_1_2_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 +24 -6
- pyafv/_version.py +2 -0
- pyafv/backend.py +9 -5
- pyafv/cell_geom.cpython-312-x86_64-linux-musl.so +0 -0
- pyafv/cell_geom_fallback.py +249 -0
- pyafv/{finite_voronoi_fast.py → finite_voronoi.py} +95 -28
- pyafv/physical_params.py +57 -14
- {pyafv-0.3.3.dist-info → pyafv-0.3.4.dist-info}/METADATA +9 -3
- pyafv-0.3.4.dist-info/RECORD +12 -0
- pyafv/finite_voronoi_fallback.py +0 -989
- pyafv/simulator.py +0 -37
- pyafv-0.3.3.dist-info/RECORD +0 -12
- {pyafv-0.3.3.dist-info → pyafv-0.3.4.dist-info}/WHEEL +0 -0
- {pyafv-0.3.3.dist-info → pyafv-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {pyafv-0.3.3.dist-info → pyafv-0.3.4.dist-info}/top_level.txt +0 -0
pyafv/__init__.py
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""
|
|
2
|
+
PyAFV - A Python implementation of the active-finite-Voronoi (AFV) model in 2D.
|
|
3
|
+
|
|
4
|
+
**Classes**
|
|
5
|
+
|
|
6
|
+
.. autosummary::
|
|
7
|
+
:nosignatures:
|
|
8
|
+
|
|
9
|
+
PhysicalParams
|
|
10
|
+
FiniteVoronoiSimulator
|
|
11
|
+
|
|
12
|
+
**Functions**
|
|
3
13
|
|
|
4
|
-
|
|
14
|
+
.. autosummary::
|
|
15
|
+
:nosignatures:
|
|
16
|
+
|
|
17
|
+
target_delta
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .physical_params import PhysicalParams, target_delta
|
|
21
|
+
from .finite_voronoi import FiniteVoronoiSimulator
|
|
5
22
|
|
|
6
23
|
try:
|
|
7
|
-
|
|
8
|
-
except
|
|
9
|
-
__version__ = "unknown"
|
|
24
|
+
from ._version import __version__
|
|
25
|
+
except ImportError:
|
|
26
|
+
__version__ = "unknown"
|
|
27
|
+
|
|
10
28
|
|
|
11
29
|
__all__ = [
|
|
12
30
|
"PhysicalParams",
|
pyafv/_version.py
ADDED
pyafv/backend.py
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# chooses fast vs fallback implementation
|
|
2
2
|
|
|
3
3
|
try:
|
|
4
|
-
from . import
|
|
4
|
+
from . import cell_geom as _impl
|
|
5
5
|
_BACKEND_NAME = "cython"
|
|
6
|
-
|
|
6
|
+
_IMPORT_ERROR = None
|
|
7
|
+
except Exception as e: # pragma: no cover
|
|
8
|
+
from . import cell_geom_fallback as _impl
|
|
7
9
|
_BACKEND_NAME = "python"
|
|
8
|
-
|
|
10
|
+
_IMPORT_ERROR = e
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
# ---- for explicit API ----
|
|
11
|
-
|
|
14
|
+
backend_impl = _impl
|
|
12
15
|
|
|
13
16
|
__all__ = [
|
|
14
|
-
"
|
|
17
|
+
"backend_impl",
|
|
15
18
|
"_BACKEND_NAME",
|
|
19
|
+
"_IMPORT_ERROR"
|
|
16
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, 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) * 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
|
|
@@ -12,16 +12,14 @@ Key public entry points:
|
|
|
12
12
|
- update_params(): update physical parameters.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from typing import Dict, List, Tuple, Optional
|
|
15
|
+
from typing import Dict, List, Tuple, Optional, Literal
|
|
16
16
|
import numpy as np
|
|
17
17
|
from scipy.spatial import Voronoi
|
|
18
18
|
from matplotlib import pyplot as plt
|
|
19
19
|
from matplotlib.axes import Axes
|
|
20
20
|
|
|
21
21
|
from .physical_params import PhysicalParams
|
|
22
|
-
|
|
23
|
-
from .cell_geom import build_vertexpair_and_vertexpoints_cy
|
|
24
|
-
from .cell_geom import pad_regions_cy, build_point_edges_cy, compute_vertex_derivatives_cy
|
|
22
|
+
from .backend import backend_impl, _BACKEND_NAME
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
# ---- tiny helpers to avoid tiny allocations in hot loops ----
|
|
@@ -31,10 +29,57 @@ def _row_dot(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
class FiniteVoronoiSimulator:
|
|
34
|
-
|
|
32
|
+
"""Simulator for the active-finite-Voronoi (AFV) model.
|
|
33
|
+
|
|
34
|
+
This class provides an interface to simulate finite Voronoi models.
|
|
35
|
+
It wraps around the two backend implementations, which may be
|
|
36
|
+
either a Cython-accelerated version or a pure Python fallback.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, pts: np.ndarray, phys: PhysicalParams, backend: Optional[Literal["cython", "python"]] = None):
|
|
41
|
+
"""
|
|
42
|
+
Constructor of the simulator.
|
|
43
|
+
Generates a warning if the pure Python implementation is used unless explicitly requested.
|
|
44
|
+
|
|
45
|
+
:param pts: (N,2) array of initial cell center positions.
|
|
46
|
+
:type pts: numpy.ndarray
|
|
47
|
+
|
|
48
|
+
:param phys: Physical parameters used within this simulator.
|
|
49
|
+
:type phys: PhysicalParams
|
|
50
|
+
|
|
51
|
+
:param backend: Optional, specify "python" to force the use of the pure Python fallback implementation.
|
|
52
|
+
:type backend: str or None
|
|
53
|
+
"""
|
|
54
|
+
pts = np.asarray(pts, dtype=float)
|
|
55
|
+
N, dim = pts.shape
|
|
56
|
+
if dim != 2:
|
|
57
|
+
raise ValueError("pts must have shape (N,2)")
|
|
58
|
+
if not isinstance(phys, PhysicalParams):
|
|
59
|
+
raise TypeError("phys must be an instance of PhysicalParams")
|
|
60
|
+
|
|
35
61
|
self.pts = pts.copy() # (N,2) array of initial points
|
|
36
|
-
self.N =
|
|
62
|
+
self.N = N # Number of points
|
|
37
63
|
self.phys = phys
|
|
64
|
+
|
|
65
|
+
if backend != "python":
|
|
66
|
+
self._BACKEND = _BACKEND_NAME
|
|
67
|
+
self._impl = backend_impl
|
|
68
|
+
|
|
69
|
+
if self._BACKEND not in {"cython", "numba"}: # pragma: no cover
|
|
70
|
+
# raise warning to inform user about fallback
|
|
71
|
+
import warnings
|
|
72
|
+
warnings.warn(
|
|
73
|
+
"Could not import the Cython-built extension module. "
|
|
74
|
+
"Falling back to the pure Python implementation, which may be slower. "
|
|
75
|
+
"To enable the accelerated version, ensure that all dependencies are installed.",
|
|
76
|
+
RuntimeWarning,
|
|
77
|
+
stacklevel=2,
|
|
78
|
+
)
|
|
79
|
+
else: # force the use of the pure Python fallback implementation.
|
|
80
|
+
self._BACKEND = "python"
|
|
81
|
+
from . import cell_geom_fallback as _impl
|
|
82
|
+
self._impl = _impl
|
|
38
83
|
|
|
39
84
|
# --------------------- Voronoi construction & extension ---------------------
|
|
40
85
|
def _build_voronoi_with_extensions(self) -> Tuple[Voronoi, np.ndarray, List[List[int]], int, Dict[Tuple[int,int], int], Dict[int, List[int]]]:
|
|
@@ -189,8 +234,8 @@ class FiniteVoronoiSimulator:
|
|
|
189
234
|
# number of native (finite) vertices
|
|
190
235
|
num_vertices = len(vor.vertices)
|
|
191
236
|
|
|
192
|
-
# Build vertexpair2ridge and vertex_points using Cython function
|
|
193
|
-
vertexpair2ridge, vertex_points =
|
|
237
|
+
# Build vertexpair2ridge and vertex_points using Cython/Python backend function
|
|
238
|
+
vertexpair2ridge, vertex_points = self._impl.build_vertexpair_and_vertexpoints(ridge_vertices_all, vor.ridge_points, num_vertices, N)
|
|
194
239
|
|
|
195
240
|
return vor, vertices_all, ridge_vertices_all, num_vertices, vertexpair2ridge, vertex_points
|
|
196
241
|
|
|
@@ -339,8 +384,8 @@ class FiniteVoronoiSimulator:
|
|
|
339
384
|
# --------------------------------------------------
|
|
340
385
|
# Part 1 in Cython
|
|
341
386
|
# --------------------------------------------------
|
|
342
|
-
vor_regions =
|
|
343
|
-
point_edges_type, point_vertices_f_idx =
|
|
387
|
+
vor_regions = self._impl.pad_regions(vor.regions) # (R, Kmax) int64 with -1 padding
|
|
388
|
+
point_edges_type, point_vertices_f_idx = self._impl.build_point_edges(
|
|
344
389
|
vor_regions, vor.point_region.astype(np.int64),
|
|
345
390
|
vertices_all.astype(np.float64), pts.astype(np.float64),
|
|
346
391
|
int(num_vertices), vertexpair2ridge,
|
|
@@ -351,7 +396,7 @@ class FiniteVoronoiSimulator:
|
|
|
351
396
|
# --------------------------------------------------
|
|
352
397
|
# Part 2 in Cython
|
|
353
398
|
# --------------------------------------------------
|
|
354
|
-
vertex_out_da_dtheta, vertex_out_dl_dtheta, dA_poly_dh, dP_poly_dh, area_list, perimeter_list =
|
|
399
|
+
vertex_out_da_dtheta, vertex_out_dl_dtheta, dA_poly_dh, dP_poly_dh, area_list, perimeter_list = self._impl.compute_vertex_derivatives(
|
|
355
400
|
point_edges_type, # list-of-lists / arrays of edge types
|
|
356
401
|
point_vertices_f_idx, # list-of-lists / arrays of vertex ids
|
|
357
402
|
vertices_all.astype(np.float64, copy=False),
|
|
@@ -608,14 +653,26 @@ class FiniteVoronoiSimulator:
|
|
|
608
653
|
return F
|
|
609
654
|
|
|
610
655
|
# --------------------- One integration step ---------------------
|
|
611
|
-
def build(self) ->
|
|
612
|
-
"""
|
|
656
|
+
def build(self) -> dict:
|
|
657
|
+
""" Build the finite-Voronoi structure and compute forces, returning a dictionary of diagnostics.
|
|
658
|
+
|
|
613
659
|
Do the following:
|
|
614
660
|
- Build Voronoi (+ extensions)
|
|
615
661
|
- Get cell connectivity
|
|
616
662
|
- Compute per-cell quantities and derivatives
|
|
617
663
|
- Assemble forces
|
|
618
|
-
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
:return: A dictionary containing the geometric properties:
|
|
667
|
+
|
|
668
|
+
* **forces**: (N,2) array of forces on cell centers
|
|
669
|
+
* **areas**: (N,) array of cell areas
|
|
670
|
+
* **perimeters**: (N,) array of cell perimeters
|
|
671
|
+
* **vertices**: (M,2) array of all Voronoi + extension vertices
|
|
672
|
+
* **edges_type**: list-of-lists of edge types per cell (1=straight, 0=circular arc)
|
|
673
|
+
* **regions**: list-of-lists of vertex indices per cell
|
|
674
|
+
* **connections**: (M',2) array of connected cell index pairs
|
|
675
|
+
:rtype: dict
|
|
619
676
|
"""
|
|
620
677
|
(vor, vertices_all, ridge_vertices_all, num_vertices,
|
|
621
678
|
vertexpair2ridge, vertex_points) = self._build_voronoi_with_extensions()
|
|
@@ -652,18 +709,17 @@ class FiniteVoronoiSimulator:
|
|
|
652
709
|
# --------------------- 2D plotting utilities ---------------------
|
|
653
710
|
def plot_2d(self, ax: Optional[Axes] = None, show: bool = False) -> Axes:
|
|
654
711
|
"""
|
|
655
|
-
Build the Voronoi
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
ax
|
|
660
|
-
|
|
661
|
-
show
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
ax : matplotlib.axes.Axes
|
|
712
|
+
Build the finite-Voronoi structure and render a 2D snapshot.
|
|
713
|
+
|
|
714
|
+
Basically a wrapper of :py:func:`build()` function + plot.
|
|
715
|
+
|
|
716
|
+
:param ax: If provided, draw into this axes; otherwise get the current axes.
|
|
717
|
+
:type ax: matplotlib.axes.Axes or None
|
|
718
|
+
:param show: Whether to call plt.show() at the end.
|
|
719
|
+
:type show: bool
|
|
720
|
+
|
|
721
|
+
:return: The matplotlib axes containing the plot.
|
|
722
|
+
:rtype: matplotlib.axes.Axes
|
|
667
723
|
"""
|
|
668
724
|
(vor, vertices_all, ridge_vertices_all, num_vertices,
|
|
669
725
|
vertexpair2ridge, vertex_points) = self._build_voronoi_with_extensions()
|
|
@@ -787,16 +843,27 @@ class FiniteVoronoiSimulator:
|
|
|
787
843
|
def update_positions(self, pts: np.ndarray) -> None:
|
|
788
844
|
"""
|
|
789
845
|
Update cell center positions.
|
|
846
|
+
|
|
847
|
+
:param pts: ndarray of shape (N,2)
|
|
848
|
+
:type pts: numpy.ndarray
|
|
790
849
|
"""
|
|
791
|
-
|
|
850
|
+
pts = np.asarray(pts, dtype=float)
|
|
851
|
+
N, dim = pts.shape
|
|
792
852
|
if dim != 2:
|
|
793
|
-
raise ValueError("
|
|
853
|
+
raise ValueError("pts must have shape (N,2)")
|
|
794
854
|
|
|
795
855
|
self.pts = pts
|
|
856
|
+
self.N = N
|
|
796
857
|
|
|
797
858
|
# --------------------- Update physical parameters ---------------------
|
|
798
859
|
def update_params(self, phys: PhysicalParams) -> None:
|
|
799
860
|
"""
|
|
800
861
|
Update physical parameters.
|
|
862
|
+
|
|
863
|
+
:param phys: PhysicalParams object
|
|
864
|
+
:type phys: PhysicalParams
|
|
801
865
|
"""
|
|
866
|
+
if not isinstance(phys, PhysicalParams):
|
|
867
|
+
raise TypeError("phys must be an instance of PhysicalParams")
|
|
868
|
+
|
|
802
869
|
self.phys = phys
|
pyafv/physical_params.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import numpy as np
|
|
2
3
|
from scipy.optimize import minimize
|
|
3
4
|
from dataclasses import dataclass, replace
|
|
@@ -15,30 +16,63 @@ def sigmoid(x):
|
|
|
15
16
|
|
|
16
17
|
@dataclass(frozen=True)
|
|
17
18
|
class PhysicalParams:
|
|
18
|
-
|
|
19
|
+
"""Physical parameters for the active-finite-Voronoi (AFV) model.
|
|
20
|
+
|
|
21
|
+
Caveat:
|
|
22
|
+
Frozen dataclass is used to ensure immutability of instances.
|
|
23
|
+
"""
|
|
24
|
+
|
|
19
25
|
r: float = 1.0
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
"""Radius (maximal) of the Voronoi cells."""
|
|
27
|
+
|
|
28
|
+
A0: float = np.pi
|
|
29
|
+
"""Preferred area of the Voronoi cells."""
|
|
30
|
+
|
|
31
|
+
P0: float = 4.8
|
|
32
|
+
"""Preferred perimeter of the Voronoi cells."""
|
|
33
|
+
|
|
34
|
+
KA: float = 1.0
|
|
35
|
+
"""Area elasticity constant."""
|
|
36
|
+
|
|
37
|
+
KP: float = 1.0
|
|
38
|
+
"""Perimeter elasticity constant."""
|
|
39
|
+
|
|
40
|
+
lambda_tension: float = 0.2
|
|
41
|
+
"""Tension difference between non-contacting edges and contacting edges."""
|
|
42
|
+
|
|
43
|
+
delta: float = 0.0
|
|
44
|
+
"""Small offset to avoid singularities in computations."""
|
|
45
|
+
|
|
46
|
+
def get_steady_state(self) -> tuple[float, float]:
|
|
47
|
+
"""Compute steady-state (l,d) given physical params.
|
|
48
|
+
|
|
49
|
+
:return: Tuple of (l, d) at steady state.
|
|
50
|
+
:rtype: tuple[float, float]
|
|
51
|
+
"""
|
|
29
52
|
params = [self.KA, self.KP, self.A0, self.P0, self.lambda_tension]
|
|
30
53
|
result = self._minimize_energy(params, restarts=10)
|
|
31
54
|
l, d = result[0]
|
|
32
55
|
return l, d
|
|
33
56
|
|
|
34
|
-
def with_optimal_radius(self):
|
|
35
|
-
"""Returns a new instance with the radius updated to steady state.
|
|
57
|
+
def with_optimal_radius(self) -> PhysicalParams:
|
|
58
|
+
"""Returns a new instance with the radius updated to steady state.
|
|
59
|
+
|
|
60
|
+
:return: New instance with optimal radius.
|
|
61
|
+
:rtype: PhysicalParams
|
|
62
|
+
"""
|
|
36
63
|
l, d = self.get_steady_state()
|
|
37
64
|
new_params = replace(self, r=l)
|
|
38
65
|
return new_params
|
|
39
66
|
|
|
40
|
-
def with_delta(self, delta_new: float):
|
|
41
|
-
"""Returns a new instance with the specified delta.
|
|
67
|
+
def with_delta(self, delta_new: float) -> PhysicalParams:
|
|
68
|
+
"""Returns a new instance with the specified delta.
|
|
69
|
+
|
|
70
|
+
:param delta_new: New delta value.
|
|
71
|
+
:type delta_new: float
|
|
72
|
+
|
|
73
|
+
:return: New instance with updated delta.
|
|
74
|
+
:rtype: PhysicalParams
|
|
75
|
+
"""
|
|
42
76
|
return replace(self, delta=delta_new)
|
|
43
77
|
|
|
44
78
|
def _energy_unconstrained(self, z, params):
|
|
@@ -81,6 +115,15 @@ class PhysicalParams:
|
|
|
81
115
|
def target_delta(params: PhysicalParams, target_force: float) -> float:
|
|
82
116
|
"""
|
|
83
117
|
Given physical parameters and a target detachment force, compute the corresponding delta.
|
|
118
|
+
|
|
119
|
+
:param params: Physical parameters of the AFV model.
|
|
120
|
+
:type params: PhysicalParams
|
|
121
|
+
|
|
122
|
+
:param target_force: Target detachment force.
|
|
123
|
+
:type target_force: float
|
|
124
|
+
|
|
125
|
+
:return: Corresponding delta value.
|
|
126
|
+
:rtype: float
|
|
84
127
|
"""
|
|
85
128
|
KP, A0, P0, Lambda = params.KP, params.A0, params.P0, params.lambda_tension
|
|
86
129
|
l = params.r
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyafv
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Python implementation of the active-finite-Voronoi (AFV) model
|
|
5
5
|
Author-email: "Wei Wang (汪巍)" <ww000721@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -48,11 +48,16 @@ The AFV framework was introduced and developed in, for example, Refs. [[1](#huan
|
|
|
48
48
|
|
|
49
49
|
## Installation
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
To install **PyAFV** with `pip`, run:
|
|
52
52
|
```bash
|
|
53
53
|
pip install pyafv
|
|
54
54
|
```
|
|
55
|
-
|
|
55
|
+
The package supports Python ≥ 3.9 and < 3.15.
|
|
56
|
+
To verify that the installation was successful and that the correct version is installed, run the following in Python:
|
|
57
|
+
```python
|
|
58
|
+
import pyafv
|
|
59
|
+
print(pyafv.__version__)
|
|
60
|
+
```
|
|
56
61
|
|
|
57
62
|
|
|
58
63
|
## Usage
|
|
@@ -85,6 +90,7 @@ See important [**issues**](https://github.com/wwang721/pyafv/issues?q=is%3Aissue
|
|
|
85
90
|
* [Add customized plotting to examples illustrating access to vertices and edges #5](https://github.com/wwang721/pyafv/issues/5) [Completed in PR [#7](https://github.com/wwang721/pyafv/pull/7)]
|
|
86
91
|
* [Time step dependence of intercellular adhesion in simulations #8](https://github.com/wwang721/pyafv/issues/8) [Closed in PR [#9](https://github.com/wwang721/pyafv/pull/9)]
|
|
87
92
|
|
|
93
|
+
Full documentation on [readthedocs](https://pyafv.readthedocs.io/en/latest/)!
|
|
88
94
|
|
|
89
95
|
## References
|
|
90
96
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyafv/__init__.py,sha256=3opuTLXyu3ry_Gzxjlb4PFygGOzS-m3NOADkII9B-Mk,540
|
|
2
|
+
pyafv/_version.py,sha256=oAsKh6W1X21Eok94R7mDW45wpERdFHwJKBZAJCO4paw,42
|
|
3
|
+
pyafv/backend.py,sha256=O15-rtzPQvTl_fIyZu4L0LU8UG4uRnAuq4bi5HBlKy8,418
|
|
4
|
+
pyafv/cell_geom.cpython-312-x86_64-linux-musl.so,sha256=6pzMkPrCjNqS2SrSrs5B1IUhPTCanBfyXcBemAyticA,1831992
|
|
5
|
+
pyafv/cell_geom_fallback.py,sha256=teA-qK38eiHMdhM8ULRBCYWccNPLp17PRS_jEl1uGjU,9127
|
|
6
|
+
pyafv/finite_voronoi.py,sha256=JA3o9RctDQ6l8keEeu0UcHp33DdoEdEg6fvyCUAOZdI,39148
|
|
7
|
+
pyafv/physical_params.py,sha256=3Ryf2Jcm1xQuRNdR1yunTChjnG3UveyIpfXFA9gG10A,4823
|
|
8
|
+
pyafv-0.3.4.dist-info/METADATA,sha256=N6cBcjMiU9eTk0E_5hS3wOrynU1FgDpymFeFgZxEd8M,5390
|
|
9
|
+
pyafv-0.3.4.dist-info/WHEEL,sha256=AwHYJA1Do1jwgPIoLQR4DiHSeYY_vU6Ht9Vljq5Yt_M,112
|
|
10
|
+
pyafv-0.3.4.dist-info/top_level.txt,sha256=mrKQNqc4GQxuZ7hd5UrKxbA_AJsuSqiJyMxL7Nu7va0,6
|
|
11
|
+
pyafv-0.3.4.dist-info/RECORD,,
|
|
12
|
+
pyafv-0.3.4.dist-info/licenses/LICENSE,sha256=TNjxBxdiOzyewn2FP7g1FjW3CJoxiGCY6ShPVFv02G8,1065
|