pyafv 0.3.3__cp312-cp312-win_arm64.whl → 0.3.6__cp312-cp312-win_arm64.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 +9 -6
- pyafv/_version.py +2 -0
- pyafv/backend.py +9 -5
- pyafv/cell_geom.cp312-win_arm64.pyd +0 -0
- pyafv/cell_geom_fallback.py +249 -0
- pyafv/{finite_voronoi_fast.py → finite_voronoi.py} +236 -53
- pyafv/physical_params.py +58 -18
- {pyafv-0.3.3.dist-info → pyafv-0.3.6.dist-info}/METADATA +13 -5
- pyafv-0.3.6.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.6.dist-info}/WHEEL +0 -0
- {pyafv-0.3.3.dist-info → pyafv-0.3.6.dist-info}/licenses/LICENSE +0 -0
- {pyafv-0.3.3.dist-info → pyafv-0.3.6.dist-info}/top_level.txt +0 -0
|
@@ -12,16 +12,19 @@ Key public entry points:
|
|
|
12
12
|
- update_params(): update physical parameters.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
from scipy.spatial import Voronoi
|
|
18
|
-
from matplotlib import pyplot as plt
|
|
19
|
-
from matplotlib.axes import Axes
|
|
15
|
+
# Enable postponed evaluation of annotations
|
|
16
|
+
from __future__ import annotations
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
# Only import typing modules when type checking, e.g., in VS Code or IDEs.
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
21
|
+
from scipy.spatial import Voronoi
|
|
22
|
+
import matplotlib.axes
|
|
23
|
+
import numpy
|
|
24
|
+
import typing
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
from .
|
|
26
|
+
import numpy as np
|
|
27
|
+
from .physical_params import PhysicalParams
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
# ---- tiny helpers to avoid tiny allocations in hot loops ----
|
|
@@ -31,18 +34,86 @@ def _row_dot(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
class FiniteVoronoiSimulator:
|
|
34
|
-
|
|
37
|
+
"""Simulator for the active-finite-Voronoi (AFV) model.
|
|
38
|
+
|
|
39
|
+
This class provides an interface to simulate the finite Voronoi model.
|
|
40
|
+
It wraps around the two backend implementations, which may be
|
|
41
|
+
either a Cython-accelerated version or a pure Python fallback.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
pts: (N,2) array of initial cell center positions.
|
|
45
|
+
phys: Physical parameters used within this simulator.
|
|
46
|
+
backend: Optional, specify "python" to force the use of the pure Python fallback implementation.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If *pts* does not have shape (N,2).
|
|
50
|
+
TypeError: If *phys* is not an instance of PhysicalParams.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, pts: numpy.ndarray, phys: PhysicalParams, backend: typing.Literal["cython", "python"] | None = None):
|
|
54
|
+
"""
|
|
55
|
+
Constructor of the simulator.
|
|
56
|
+
"""
|
|
57
|
+
from .backend import backend_impl, _BACKEND_NAME
|
|
58
|
+
from scipy.spatial import Voronoi
|
|
59
|
+
self._voronoi = Voronoi
|
|
60
|
+
|
|
61
|
+
pts = np.asarray(pts, dtype=float)
|
|
62
|
+
if pts.ndim != 2 or pts.shape[1] != 2:
|
|
63
|
+
raise ValueError("pts must have shape (N,2)")
|
|
64
|
+
if not isinstance(phys, PhysicalParams):
|
|
65
|
+
raise TypeError("phys must be an instance of PhysicalParams")
|
|
66
|
+
|
|
35
67
|
self.pts = pts.copy() # (N,2) array of initial points
|
|
36
68
|
self.N = pts.shape[0] # Number of points
|
|
37
69
|
self.phys = phys
|
|
70
|
+
self._preferred_areas = np.full(self.N, phys.A0, dtype=float) # (N,) preferred areas A0
|
|
71
|
+
|
|
72
|
+
if backend != "python":
|
|
73
|
+
self._BACKEND = _BACKEND_NAME
|
|
74
|
+
self._impl = backend_impl
|
|
75
|
+
|
|
76
|
+
if self._BACKEND not in {"cython", "numba"}: # pragma: no cover
|
|
77
|
+
# raise warning to inform user about fallback
|
|
78
|
+
import warnings
|
|
79
|
+
warnings.warn(
|
|
80
|
+
"Could not import the Cython-built extension module. "
|
|
81
|
+
"Falling back to the pure Python implementation, which may be slower. "
|
|
82
|
+
"To enable the accelerated version, ensure that all dependencies are installed.",
|
|
83
|
+
RuntimeWarning,
|
|
84
|
+
stacklevel=2,
|
|
85
|
+
)
|
|
86
|
+
else: # force the use of the pure Python fallback implementation.
|
|
87
|
+
self._BACKEND = "python"
|
|
88
|
+
from . import cell_geom_fallback as _impl
|
|
89
|
+
self._impl = _impl
|
|
38
90
|
|
|
39
91
|
# --------------------- Voronoi construction & extension ---------------------
|
|
40
|
-
def _build_voronoi_with_extensions(self) ->
|
|
92
|
+
def _build_voronoi_with_extensions(self) -> tuple[Voronoi, np.ndarray, list[list[int]], int, dict[tuple[int,int], int], dict[int, list[int]]]:
|
|
41
93
|
"""
|
|
42
|
-
Build
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
Build standard Voronoi structure for current points.
|
|
95
|
+
|
|
96
|
+
For N<=2, emulate regions.
|
|
97
|
+
For N>=3, extend infinite ridges, add extension vertices, and update
|
|
98
|
+
regions accordingly. Return the augmented structures.
|
|
99
|
+
|
|
100
|
+
.. warning::
|
|
101
|
+
|
|
102
|
+
This is an internal method. Use with caution.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
tuple[scipy.spatial.Voronoi, numpy.ndarray, list[list[int]], int, dict[tuple[int,int], int], dict[int, list[int]]] : A tuple containing:
|
|
106
|
+
|
|
107
|
+
- **vor**: SciPy Voronoi object for current points with extensions.
|
|
108
|
+
- **vertices_all**: (M,2) array of all Voronoi vertices including extensions.
|
|
109
|
+
- **ridge_vertices_all**: list of lists of vertex indices for each ridge, including extensions.
|
|
110
|
+
- **num_vertices**: Number of Voronoi vertices before adding extension.
|
|
111
|
+
- **vertexpair2ridge**: dict mapping vertex index pairs to ridge index.
|
|
112
|
+
- **vertex_points**: dict mapping vertex index to list of associated point indices.
|
|
45
113
|
"""
|
|
114
|
+
|
|
115
|
+
Voronoi = self._voronoi
|
|
116
|
+
|
|
46
117
|
r = self.phys.r
|
|
47
118
|
pts = self.pts
|
|
48
119
|
N = self.N
|
|
@@ -189,24 +260,53 @@ class FiniteVoronoiSimulator:
|
|
|
189
260
|
# number of native (finite) vertices
|
|
190
261
|
num_vertices = len(vor.vertices)
|
|
191
262
|
|
|
192
|
-
# Build vertexpair2ridge and vertex_points using Cython function
|
|
193
|
-
vertexpair2ridge, vertex_points =
|
|
263
|
+
# Build vertexpair2ridge and vertex_points using Cython/Python backend function
|
|
264
|
+
vertexpair2ridge, vertex_points = self._impl.build_vertexpair_and_vertexpoints(ridge_vertices_all, vor.ridge_points, num_vertices, N)
|
|
194
265
|
|
|
195
266
|
return vor, vertices_all, ridge_vertices_all, num_vertices, vertexpair2ridge, vertex_points
|
|
196
267
|
|
|
197
268
|
# --------------------- Geometry & energy contributions per cell ---------------------
|
|
198
|
-
def _per_cell_geometry(self, vor: Voronoi, vertices_all: np.ndarray, ridge_vertices_all: np.ndarray, num_vertices: int, vertexpair2ridge:
|
|
269
|
+
def _per_cell_geometry(self, vor: Voronoi, vertices_all: np.ndarray, ridge_vertices_all: np.ndarray, num_vertices: int, vertexpair2ridge: dict[tuple[int, int], int]) -> dict[str,object]:
|
|
199
270
|
"""
|
|
200
|
-
|
|
271
|
+
Build the finite-Voronoi per-cell geometry and energy contributions.
|
|
272
|
+
|
|
273
|
+
Iterate each cell to:
|
|
201
274
|
- sort polygon/arc vertices around each cell
|
|
202
275
|
- classify edges (1 = straight Voronoi edge; 0 = circular arc)
|
|
203
276
|
- compute area/perimeter for each cell
|
|
204
277
|
- accumulate derivatives w.r.t. vertices (dA_poly/dh, dP_poly/dh)
|
|
205
|
-
- register
|
|
278
|
+
- register "outer" vertices created at arc intersections and track their point pairs
|
|
279
|
+
|
|
280
|
+
.. warning::
|
|
281
|
+
This is an internal method. Use with caution.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
vor: SciPy Voronoi object for current points with extensions.
|
|
285
|
+
vertices_all: (M,2) array of all Voronoi vertices including extensions.
|
|
286
|
+
ridge_vertices_all: list of lists of vertex indices for each ridge, including extensions.
|
|
287
|
+
num_vertices: Number of Voronoi vertices before adding extension.
|
|
288
|
+
vertexpair2ridge: dict mapping vertex index pairs to ridge index.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
dict[str, object]: A diagnostics dictionary containing:
|
|
292
|
+
|
|
293
|
+
- **vertex_in_id**: set of inner vertex ids.
|
|
294
|
+
- **vertex_out_id**: set of outer vertex ids.
|
|
295
|
+
- **vertices_out**: (L,2) array of outer vertex coordinates.
|
|
296
|
+
- **vertex_out_points**: (L,2) array of point index pairs associated with each outer vertex.
|
|
297
|
+
- **vertex_out_da_dtheta**: array of dA/dtheta for all outer vertices.
|
|
298
|
+
- **vertex_out_dl_dtheta**: array of dL/dtheta for all outer vertices.
|
|
299
|
+
- **dA_poly_dh**: array of dA_polygon/dh for each vertex.
|
|
300
|
+
- **dP_poly_dh**: array of dP_polygon/dh for each vertex.
|
|
301
|
+
- **area_list**: array of polygon areas for each cell.
|
|
302
|
+
- **perimeter_list**: array of polygon perimeters for each cell.
|
|
303
|
+
- **point_edges_type**: list of lists of edge types per cell.
|
|
304
|
+
- **point_vertices_f_idx**: list of lists of vertex ids per cell.
|
|
305
|
+
- **num_vertices_ext**: number of vertices including infinite extension vertices.
|
|
206
306
|
"""
|
|
207
307
|
N = self.N
|
|
208
308
|
r = self.phys.r
|
|
209
|
-
|
|
309
|
+
A0_list = self._preferred_areas
|
|
210
310
|
P0 = self.phys.P0
|
|
211
311
|
pts = self.pts
|
|
212
312
|
|
|
@@ -337,10 +437,10 @@ class FiniteVoronoiSimulator:
|
|
|
337
437
|
vertices_all = np.vstack([vertices_all, vertices_out])
|
|
338
438
|
|
|
339
439
|
# --------------------------------------------------
|
|
340
|
-
# Part 1 in Cython
|
|
440
|
+
# Part 1 in Cython/Python backend
|
|
341
441
|
# --------------------------------------------------
|
|
342
|
-
vor_regions =
|
|
343
|
-
point_edges_type, point_vertices_f_idx =
|
|
442
|
+
vor_regions = self._impl.pad_regions(vor.regions) # (R, Kmax) int64 with -1 padding
|
|
443
|
+
point_edges_type, point_vertices_f_idx = self._impl.build_point_edges(
|
|
344
444
|
vor_regions, vor.point_region.astype(np.int64),
|
|
345
445
|
vertices_all.astype(np.float64), pts.astype(np.float64),
|
|
346
446
|
int(num_vertices), vertexpair2ridge,
|
|
@@ -349,15 +449,15 @@ class FiniteVoronoiSimulator:
|
|
|
349
449
|
)
|
|
350
450
|
|
|
351
451
|
# --------------------------------------------------
|
|
352
|
-
# Part 2 in Cython
|
|
452
|
+
# Part 2 in Cython/Python backend
|
|
353
453
|
# --------------------------------------------------
|
|
354
|
-
vertex_out_da_dtheta, vertex_out_dl_dtheta, dA_poly_dh, dP_poly_dh, area_list, perimeter_list =
|
|
454
|
+
vertex_out_da_dtheta, vertex_out_dl_dtheta, dA_poly_dh, dP_poly_dh, area_list, perimeter_list = self._impl.compute_vertex_derivatives(
|
|
355
455
|
point_edges_type, # list-of-lists / arrays of edge types
|
|
356
456
|
point_vertices_f_idx, # list-of-lists / arrays of vertex ids
|
|
357
457
|
vertices_all.astype(np.float64, copy=False),
|
|
358
458
|
pts.astype(np.float64, copy=False),
|
|
359
459
|
float(r),
|
|
360
|
-
|
|
460
|
+
A0_list.astype(np.float64, copy=False),
|
|
361
461
|
float(P0),
|
|
362
462
|
int(num_vertices_ext),
|
|
363
463
|
int(num_ridges),
|
|
@@ -383,8 +483,8 @@ class FiniteVoronoiSimulator:
|
|
|
383
483
|
|
|
384
484
|
# --------------------- Force assembly ---------------------
|
|
385
485
|
def _assemble_forces(self, vertices_all: np.ndarray, num_vertices_ext: int,
|
|
386
|
-
vertex_points:
|
|
387
|
-
vertex_out_points:
|
|
486
|
+
vertex_points: dict[int, list[int]], vertex_in_id: list[int], vertex_out_id: list[int],
|
|
487
|
+
vertex_out_points: list[list[int]], vertex_out_da_dtheta: np.ndarray,
|
|
388
488
|
vertex_out_dl_dtheta: np.ndarray, dA_poly_dh: np.ndarray, dP_poly_dh: np.ndarray,
|
|
389
489
|
area_list: np.ndarray, perimeter_list: np.ndarray) -> np.ndarray:
|
|
390
490
|
"""
|
|
@@ -392,7 +492,7 @@ class FiniteVoronoiSimulator:
|
|
|
392
492
|
"""
|
|
393
493
|
N = self.N
|
|
394
494
|
r = self.phys.r
|
|
395
|
-
|
|
495
|
+
A0_list = self._preferred_areas
|
|
396
496
|
P0 = self.phys.P0
|
|
397
497
|
KA = self.phys.KA
|
|
398
498
|
KP = self.phys.KP
|
|
@@ -569,8 +669,8 @@ class FiniteVoronoiSimulator:
|
|
|
569
669
|
v_da = vertex_out_da_dtheta[h_idx] # (M,2)
|
|
570
670
|
v_dl = vertex_out_dl_dtheta[h_idx] # (M,2)
|
|
571
671
|
|
|
572
|
-
Ai_w_i = (area_list[I] -
|
|
573
|
-
Aj_w_j = (area_list[J] -
|
|
672
|
+
Ai_w_i = (area_list[I] - A0_list[I]) * v_da[:, 0]
|
|
673
|
+
Aj_w_j = (area_list[J] - A0_list[J]) * v_da[:, 1]
|
|
574
674
|
Pi_w_i = (perimeter_list[I] - P0) * v_dl[:, 0]
|
|
575
675
|
Pj_w_j = (perimeter_list[J] - P0) * v_dl[:, 1]
|
|
576
676
|
|
|
@@ -608,14 +708,26 @@ class FiniteVoronoiSimulator:
|
|
|
608
708
|
return F
|
|
609
709
|
|
|
610
710
|
# --------------------- One integration step ---------------------
|
|
611
|
-
def build(self) ->
|
|
612
|
-
"""
|
|
711
|
+
def build(self) -> dict[str, object]:
|
|
712
|
+
""" Build the finite-Voronoi structure and compute forces, returning a dictionary of diagnostics.
|
|
713
|
+
|
|
613
714
|
Do the following:
|
|
614
715
|
- Build Voronoi (+ extensions)
|
|
615
716
|
- Get cell connectivity
|
|
616
717
|
- Compute per-cell quantities and derivatives
|
|
617
718
|
- Assemble forces
|
|
618
|
-
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
dict[str, object]: A dictionary containing geometric properties with keys:
|
|
723
|
+
|
|
724
|
+
- **forces**: (N,2) array of forces on cell centers
|
|
725
|
+
- **areas**: (N,) array of cell areas
|
|
726
|
+
- **perimeters**: (N,) array of cell perimeters
|
|
727
|
+
- **vertices**: (M,2) array of all Voronoi + extension vertices
|
|
728
|
+
- **edges_type**: list-of-lists of edge types per cell (1=straight, 0=circular arc)
|
|
729
|
+
- **regions**: list-of-lists of vertex indices per cell
|
|
730
|
+
- **connections**: (M',2) array of connected cell index pairs
|
|
619
731
|
"""
|
|
620
732
|
(vor, vertices_all, ridge_vertices_all, num_vertices,
|
|
621
733
|
vertexpair2ridge, vertex_points) = self._build_voronoi_with_extensions()
|
|
@@ -650,26 +762,26 @@ class FiniteVoronoiSimulator:
|
|
|
650
762
|
)
|
|
651
763
|
|
|
652
764
|
# --------------------- 2D plotting utilities ---------------------
|
|
653
|
-
def plot_2d(self, ax:
|
|
765
|
+
def plot_2d(self, ax: matplotlib.axes.Axes | None = None, show: bool = False) -> matplotlib.axes.Axes:
|
|
654
766
|
"""
|
|
655
|
-
Build the Voronoi
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
If provided, draw into this axes; otherwise get the current axes.
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
-------
|
|
666
|
-
ax : matplotlib.axes.Axes
|
|
767
|
+
Build the finite-Voronoi structure and render a 2D snapshot.
|
|
768
|
+
|
|
769
|
+
Basically a wrapper of :py:meth:`_build_voronoi_with_extensions` and :py:meth:`_per_cell_geometry` functions + plot.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
ax: If provided, draw into this axes; otherwise get the current axes.
|
|
773
|
+
show: Whether to call ``plt.show()`` at the end.
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
The matplotlib axes containing the plot.
|
|
667
777
|
"""
|
|
668
778
|
(vor, vertices_all, ridge_vertices_all, num_vertices,
|
|
669
779
|
vertexpair2ridge, vertex_points) = self._build_voronoi_with_extensions()
|
|
670
780
|
|
|
671
781
|
geom, vertices_all = self._per_cell_geometry(vor, vertices_all, ridge_vertices_all, num_vertices, vertexpair2ridge)
|
|
672
782
|
|
|
783
|
+
from matplotlib import pyplot as plt
|
|
784
|
+
|
|
673
785
|
if ax is None:
|
|
674
786
|
ax = plt.gca()
|
|
675
787
|
|
|
@@ -681,8 +793,8 @@ class FiniteVoronoiSimulator:
|
|
|
681
793
|
return ax
|
|
682
794
|
|
|
683
795
|
# --------------------- Paradigm of plotting ---------------------
|
|
684
|
-
def _plot_routine(self, ax: Axes, vor: Voronoi, vertices_all: np.ndarray, ridge_vertices_all:
|
|
685
|
-
point_edges_type:
|
|
796
|
+
def _plot_routine(self, ax: Axes, vor: Voronoi, vertices_all: np.ndarray, ridge_vertices_all: list[list[int]],
|
|
797
|
+
point_edges_type: list[list[int]], point_vertices_f_idx: list[list[int]]) -> None:
|
|
686
798
|
"""
|
|
687
799
|
Low-level plot routine. Draws:
|
|
688
800
|
- All Voronoi edges (solid for finite, dashed for formerly-infinite)
|
|
@@ -746,7 +858,7 @@ class FiniteVoronoiSimulator:
|
|
|
746
858
|
ax.set_ylim(center[1]-L, center[1]+L)
|
|
747
859
|
|
|
748
860
|
# --------------------- Connections between cells ---------------------
|
|
749
|
-
def _get_connections(self, ridge_points:
|
|
861
|
+
def _get_connections(self, ridge_points: list[list[int]], vertices_all: np.ndarray, ridge_vertices_all: list[list[int]]) -> np.ndarray:
|
|
750
862
|
"""
|
|
751
863
|
Determine which pairs of cells are connected, i.e.,
|
|
752
864
|
the distance from the cell center to its corresponding Voronoi ridge
|
|
@@ -784,19 +896,90 @@ class FiniteVoronoiSimulator:
|
|
|
784
896
|
return connect
|
|
785
897
|
|
|
786
898
|
# --------------------- Update positions ---------------------
|
|
787
|
-
def update_positions(self, pts:
|
|
899
|
+
def update_positions(self, pts: numpy.ndarray, A0: float | numpy.ndarray | None = None) -> None:
|
|
788
900
|
"""
|
|
789
901
|
Update cell center positions.
|
|
902
|
+
|
|
903
|
+
.. note::
|
|
904
|
+
If the number of cells changes, the preferred areas for all cells
|
|
905
|
+
are reset to the default value---defined either at simulator instantiation
|
|
906
|
+
or by :py:meth:`update_params`---unless *A0* is explicitly specified.
|
|
907
|
+
|
|
908
|
+
Args:
|
|
909
|
+
pts: New cell center positions.
|
|
910
|
+
A0: Optional, set new preferred area(s).
|
|
911
|
+
|
|
912
|
+
Raises:
|
|
913
|
+
ValueError: If *pts* does not have shape (N,2).
|
|
914
|
+
ValueError: If *A0* is an array and does not have shape (N,).
|
|
790
915
|
"""
|
|
791
|
-
|
|
792
|
-
if
|
|
793
|
-
raise ValueError("
|
|
916
|
+
pts = np.asarray(pts, dtype=float)
|
|
917
|
+
if pts.ndim != 2 or pts.shape[1] != 2:
|
|
918
|
+
raise ValueError("pts must have shape (N,2)")
|
|
794
919
|
|
|
920
|
+
N = pts.shape[0]
|
|
795
921
|
self.pts = pts
|
|
796
922
|
|
|
923
|
+
if N != self.N:
|
|
924
|
+
self.N = N
|
|
925
|
+
if A0 is None:
|
|
926
|
+
self._preferred_areas = np.full(N, self.phys.A0, dtype=float)
|
|
927
|
+
else:
|
|
928
|
+
self.update_preferred_areas(A0)
|
|
929
|
+
else:
|
|
930
|
+
if A0 is not None:
|
|
931
|
+
self.update_preferred_areas(A0)
|
|
932
|
+
|
|
797
933
|
# --------------------- Update physical parameters ---------------------
|
|
798
934
|
def update_params(self, phys: PhysicalParams) -> None:
|
|
799
935
|
"""
|
|
800
936
|
Update physical parameters.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
phys: New PhysicalParams object.
|
|
940
|
+
|
|
941
|
+
Raises:
|
|
942
|
+
TypeError: If *phys* is not an instance of PhysicalParams.
|
|
943
|
+
|
|
944
|
+
.. warning::
|
|
945
|
+
This also resets all preferred cell areas to the new value of *A0*.
|
|
801
946
|
"""
|
|
947
|
+
if not isinstance(phys, PhysicalParams):
|
|
948
|
+
raise TypeError("phys must be an instance of PhysicalParams")
|
|
949
|
+
|
|
802
950
|
self.phys = phys
|
|
951
|
+
self.update_preferred_areas(phys.A0)
|
|
952
|
+
|
|
953
|
+
# --------------------- Update preferred area list ---------------------
|
|
954
|
+
def update_preferred_areas(self, A0: float | numpy.ndarray) -> None:
|
|
955
|
+
"""
|
|
956
|
+
Update the preferred areas for all cells.
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
A0: New preferred area(s) for all cells.
|
|
960
|
+
|
|
961
|
+
Raises:
|
|
962
|
+
ValueError: If *A0* does not match cell number.
|
|
963
|
+
"""
|
|
964
|
+
arr = np.asarray(A0, dtype=float)
|
|
965
|
+
|
|
966
|
+
# Accept scalar (0-d) or length-1 array as "uniform"
|
|
967
|
+
if arr.ndim == 0:
|
|
968
|
+
arr = np.full(self.N, float(arr), dtype=float)
|
|
969
|
+
elif arr.shape == (1,):
|
|
970
|
+
arr = np.full(self.N, float(arr[0]), dtype=float)
|
|
971
|
+
else:
|
|
972
|
+
if arr.shape != (self.N,):
|
|
973
|
+
raise ValueError(f"A0 must be scalar or have shape ({self.N},)")
|
|
974
|
+
|
|
975
|
+
self._preferred_areas = arr
|
|
976
|
+
|
|
977
|
+
@property
|
|
978
|
+
def preferred_areas(self) -> np.ndarray:
|
|
979
|
+
"""
|
|
980
|
+
Preferred areas for all cells (read-only).
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
numpy.ndarray: A copy of the internal preferred area array.
|
|
984
|
+
"""
|
|
985
|
+
return self._preferred_areas.copy()
|
pyafv/physical_params.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import numpy as np
|
|
2
|
-
from scipy.optimize import minimize
|
|
3
3
|
from dataclasses import dataclass, replace
|
|
4
4
|
|
|
5
5
|
|
|
@@ -15,30 +15,60 @@ def sigmoid(x):
|
|
|
15
15
|
|
|
16
16
|
@dataclass(frozen=True)
|
|
17
17
|
class PhysicalParams:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
"""Physical parameters for the active-finite-Voronoi (AFV) model.
|
|
19
|
+
|
|
20
|
+
Caveat:
|
|
21
|
+
Frozen dataclass is used for :py:class:`PhysicalParams` to ensure immutability of instances.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
r: Radius (maximal) of the Voronoi cells.
|
|
25
|
+
A0: Preferred area of the Voronoi cells.
|
|
26
|
+
P0: Preferred perimeter of the Voronoi cells.
|
|
27
|
+
KA: Area elasticity constant.
|
|
28
|
+
KP: Perimeter elasticity constant.
|
|
29
|
+
lambda_tension: Tension difference between non-contacting edges and contacting edges.
|
|
30
|
+
delta: Small offset to avoid singularities in computations.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
r: float = 1.0 #: Radius (maximal) of the Voronoi cells.
|
|
34
|
+
A0: float = np.pi #: Preferred area of the Voronoi cells.
|
|
35
|
+
P0: float = 4.8 #: Preferred perimeter of the Voronoi cells.
|
|
36
|
+
KA: float = 1.0 #: Area elasticity constant.
|
|
37
|
+
KP: float = 1.0 #: Perimeter elasticity constant.
|
|
38
|
+
lambda_tension: float = 0.2 #: Tension difference between non-contacting edges and contacting edges.
|
|
39
|
+
delta: float = 0.0 #: Small offset to avoid singularities in computations.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_steady_state(self) -> tuple[float, float]:
|
|
43
|
+
r"""Compute steady-state (l,d) for the given physical parameters.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Steady-state :math:`(\ell,d)` values.
|
|
47
|
+
"""
|
|
29
48
|
params = [self.KA, self.KP, self.A0, self.P0, self.lambda_tension]
|
|
30
49
|
result = self._minimize_energy(params, restarts=10)
|
|
31
50
|
l, d = result[0]
|
|
32
51
|
return l, d
|
|
33
52
|
|
|
34
|
-
def with_optimal_radius(self):
|
|
35
|
-
"""Returns a new instance with the radius updated to steady state.
|
|
53
|
+
def with_optimal_radius(self) -> PhysicalParams:
|
|
54
|
+
"""Returns a new instance with the radius updated to steady state.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
New instance with optimal radius.
|
|
58
|
+
"""
|
|
36
59
|
l, d = self.get_steady_state()
|
|
37
60
|
new_params = replace(self, r=l)
|
|
38
61
|
return new_params
|
|
39
62
|
|
|
40
|
-
def with_delta(self, delta_new: float):
|
|
41
|
-
"""Returns a new instance with the specified delta.
|
|
63
|
+
def with_delta(self, delta_new: float) -> PhysicalParams:
|
|
64
|
+
"""Returns a new instance with the specified delta.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
delta_new: New delta value.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
New instance with updated delta.
|
|
71
|
+
"""
|
|
42
72
|
return replace(self, delta=delta_new)
|
|
43
73
|
|
|
44
74
|
def _energy_unconstrained(self, z, params):
|
|
@@ -55,6 +85,9 @@ class PhysicalParams:
|
|
|
55
85
|
return KA * (A - A0)**2 + KP * (P - P0)**2 + Lambda * ln
|
|
56
86
|
|
|
57
87
|
def _minimize_energy(self, params, restarts=10, seed=None):
|
|
88
|
+
|
|
89
|
+
from scipy.optimize import minimize
|
|
90
|
+
|
|
58
91
|
rng = np.random.default_rng(seed)
|
|
59
92
|
best = None
|
|
60
93
|
for _ in range(restarts):
|
|
@@ -79,8 +112,15 @@ class PhysicalParams:
|
|
|
79
112
|
|
|
80
113
|
|
|
81
114
|
def target_delta(params: PhysicalParams, target_force: float) -> float:
|
|
82
|
-
"""
|
|
83
|
-
Given physical parameters and a target detachment force, compute the corresponding delta.
|
|
115
|
+
r"""
|
|
116
|
+
Given the physical parameters and a target detachment force, compute the corresponding delta.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
params: Physical parameters of the AFV model.
|
|
120
|
+
target_force: Target detachment force.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Corresponding small cutoff :math:`\delta`'s value.
|
|
84
124
|
"""
|
|
85
125
|
KP, A0, P0, Lambda = params.KP, params.A0, params.P0, params.lambda_tension
|
|
86
126
|
l = params.r
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyafv
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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
|
|
7
7
|
Project-URL: Download, https://pypi.org/project/pyafv/#files
|
|
8
8
|
Project-URL: Source Code, https://github.com/wwang721/pyafv
|
|
9
|
+
Project-URL: Documentation, https://pyafv.readthedocs.io/
|
|
10
|
+
Project-URL: Changelog, https://github.com/wwang721/pyafv/releases/latest
|
|
9
11
|
Keywords: voronoi-model,cellular-patterns,biological-modeling
|
|
10
12
|
Classifier: Development Status :: 4 - Beta
|
|
11
13
|
Classifier: Intended Audience :: Developers
|
|
12
14
|
Classifier: Intended Audience :: Science/Research
|
|
13
15
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -19,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
21
|
Classifier: Operating System :: OS Independent
|
|
21
22
|
Classifier: Topic :: Scientific/Engineering
|
|
22
|
-
Requires-Python: <3.15,>=3.
|
|
23
|
+
Requires-Python: <3.15,>=3.10
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
License-File: LICENSE
|
|
25
26
|
Requires-Dist: numpy>=1.26.4
|
|
@@ -35,6 +36,7 @@ Dynamic: license-file
|
|
|
35
36
|
[](https://pypi.org/project/pyafv/)
|
|
36
37
|
[](https://doi.org/10.5281/zenodo.18091659)
|
|
37
38
|
<!--[](https://github.com/wwang721/pyafv/actions/workflows/tests.yml?query=branch:main)-->
|
|
39
|
+
[](https://github.com/wwang721/pyafv/actions/workflows/tests_all_platform.yml)
|
|
38
40
|
[](https://github.com/wwang721/pyafv/actions/workflows/tests.yml)
|
|
39
41
|
[](https://codecov.io/github/wwang721/pyafv/tree/main)
|
|
40
42
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -48,11 +50,16 @@ The AFV framework was introduced and developed in, for example, Refs. [[1](#huan
|
|
|
48
50
|
|
|
49
51
|
## Installation
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
To install **PyAFV** with `pip`, run:
|
|
52
54
|
```bash
|
|
53
55
|
pip install pyafv
|
|
54
56
|
```
|
|
55
|
-
|
|
57
|
+
The package supports Python ≥ 3.10 and < 3.15.
|
|
58
|
+
To verify that the installation was successful and that the correct version is installed, run the following in Python:
|
|
59
|
+
```python
|
|
60
|
+
import pyafv
|
|
61
|
+
print(pyafv.__version__)
|
|
62
|
+
```
|
|
56
63
|
|
|
57
64
|
|
|
58
65
|
## Usage
|
|
@@ -85,6 +92,7 @@ See important [**issues**](https://github.com/wwang721/pyafv/issues?q=is%3Aissue
|
|
|
85
92
|
* [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
93
|
* [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
94
|
|
|
95
|
+
Full documentation on [readthedocs](https://pyafv.readthedocs.io/en/latest/)!
|
|
88
96
|
|
|
89
97
|
## References
|
|
90
98
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyafv/__init__.py,sha256=xN4BFNCwjuWbWHEgLsUdIYuqBWTVstIm0enHNDewIUk,444
|
|
2
|
+
pyafv/_version.py,sha256=kpvCym_suX_uqk67H7D0GmkqZvRCvhh175QxhcaQnnk,44
|
|
3
|
+
pyafv/backend.py,sha256=MlQThqIlTGXt__dj44rSutZLexKJTl9NaOEZ5s1otns,438
|
|
4
|
+
pyafv/cell_geom.cp312-win_arm64.pyd,sha256=jTe3rZl46tI-W-olfAGU0S0IW4p6ha0x9nxV2EEbiwE,190976
|
|
5
|
+
pyafv/cell_geom_fallback.py,sha256=eyhDC6Qe8_E_H59pNSwXeYA1DvJW1bHEnemK2TC8P0Y,9391
|
|
6
|
+
pyafv/finite_voronoi.py,sha256=nXAWsK9sLlfRFjAc1TKsPuKLRcy9WOb9Wcjx1WfB2wQ,45449
|
|
7
|
+
pyafv/physical_params.py,sha256=hvu20tlJC7VHy8bonrBJmV-sMiIm5NCDhOAGtgixjXo,5344
|
|
8
|
+
pyafv-0.3.6.dist-info/licenses/LICENSE,sha256=UHfiLQ93gkQMOoN559ZBeG9kywkNcvDgxA9MRniWOpY,1086
|
|
9
|
+
pyafv-0.3.6.dist-info/METADATA,sha256=3hRgZSfDSnrtZyDI0Tn3ulo4D6MeXybmjnw2oq43kAU,5782
|
|
10
|
+
pyafv-0.3.6.dist-info/WHEEL,sha256=me1aG6nvouDIdjWXNa5q_zebZZEPzD53N4rwsapSjvI,101
|
|
11
|
+
pyafv-0.3.6.dist-info/top_level.txt,sha256=mrKQNqc4GQxuZ7hd5UrKxbA_AJsuSqiJyMxL7Nu7va0,6
|
|
12
|
+
pyafv-0.3.6.dist-info/RECORD,,
|