pyafv 0.3.5__cp312-cp312-win_amd64.whl → 0.4.1__cp312-cp312-win_amd64.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 +2 -0
- pyafv/_version.py +1 -1
- pyafv/calibrate/__init__.py +15 -0
- pyafv/calibrate/core.py +90 -0
- pyafv/calibrate/deformable_polygon.py +372 -0
- pyafv/cell_geom.cp310-win32.pyd +0 -0
- pyafv/cell_geom.cp310-win_amd64.pyd +0 -0
- pyafv/cell_geom.cp311-win32.pyd +0 -0
- pyafv/cell_geom.cp311-win_amd64.pyd +0 -0
- pyafv/cell_geom.cp312-win32.pyd +0 -0
- pyafv/cell_geom.cp312-win_amd64.pyd +0 -0
- pyafv/finite_voronoi.py +69 -44
- pyafv/physical_params.py +109 -39
- pyafv-0.4.1.dist-info/METADATA +172 -0
- pyafv-0.4.1.dist-info/RECORD +19 -0
- {pyafv-0.3.5.dist-info → pyafv-0.4.1.dist-info}/WHEEL +1 -2
- pyafv-0.3.5.dist-info/METADATA +0 -117
- pyafv-0.3.5.dist-info/RECORD +0 -12
- pyafv-0.3.5.dist-info/top_level.txt +0 -1
- {pyafv-0.3.5.dist-info → pyafv-0.4.1.dist-info}/licenses/LICENSE +0 -0
pyafv/finite_voronoi.py
CHANGED
|
@@ -12,14 +12,18 @@ Key public entry points:
|
|
|
12
12
|
- update_params(): update physical parameters.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from
|
|
15
|
+
# Enable postponed evaluation of annotations
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
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 typing
|
|
20
24
|
|
|
25
|
+
import numpy as np
|
|
21
26
|
from .physical_params import PhysicalParams
|
|
22
|
-
from .backend import backend_impl, _BACKEND_NAME
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
# ---- tiny helpers to avoid tiny allocations in hot loops ----
|
|
@@ -36,19 +40,26 @@ class FiniteVoronoiSimulator:
|
|
|
36
40
|
either a Cython-accelerated version or a pure Python fallback.
|
|
37
41
|
|
|
38
42
|
Args:
|
|
39
|
-
pts: (N,2) array of initial cell center positions.
|
|
43
|
+
pts (numpy.ndarray): (N,2) array of initial cell center positions.
|
|
40
44
|
phys: Physical parameters used within this simulator.
|
|
41
45
|
backend: Optional, specify "python" to force the use of the pure Python fallback implementation.
|
|
42
46
|
|
|
43
47
|
Raises:
|
|
44
48
|
ValueError: If *pts* does not have shape (N,2).
|
|
45
|
-
TypeError: If *phys* is not an instance of PhysicalParams
|
|
49
|
+
TypeError: If *phys* is not an instance of :py:class:`PhysicalParams`.
|
|
50
|
+
|
|
51
|
+
Warnings:
|
|
52
|
+
If the Cython backend cannot be imported (unless *backend* is set to "python"),
|
|
53
|
+
a **RuntimeWarning** is raised and the pure Python implementation is used instead.
|
|
46
54
|
"""
|
|
47
55
|
|
|
48
|
-
def __init__(self, pts: np.ndarray, phys: PhysicalParams, backend: Literal["cython", "python"] | None = None):
|
|
56
|
+
def __init__(self, pts: np.ndarray, phys: PhysicalParams, backend: typing.Literal["cython", "python"] | None = None):
|
|
49
57
|
"""
|
|
50
58
|
Constructor of the simulator.
|
|
51
59
|
"""
|
|
60
|
+
from scipy.spatial import Voronoi
|
|
61
|
+
self._voronoi = Voronoi
|
|
62
|
+
|
|
52
63
|
pts = np.asarray(pts, dtype=float)
|
|
53
64
|
if pts.ndim != 2 or pts.shape[1] != 2:
|
|
54
65
|
raise ValueError("pts must have shape (N,2)")
|
|
@@ -61,6 +72,7 @@ class FiniteVoronoiSimulator:
|
|
|
61
72
|
self._preferred_areas = np.full(self.N, phys.A0, dtype=float) # (N,) preferred areas A0
|
|
62
73
|
|
|
63
74
|
if backend != "python":
|
|
75
|
+
from .backend import backend_impl, _BACKEND_NAME
|
|
64
76
|
self._BACKEND = _BACKEND_NAME
|
|
65
77
|
self._impl = backend_impl
|
|
66
78
|
|
|
@@ -93,15 +105,18 @@ class FiniteVoronoiSimulator:
|
|
|
93
105
|
This is an internal method. Use with caution.
|
|
94
106
|
|
|
95
107
|
Returns:
|
|
96
|
-
tuple[scipy.spatial.Voronoi, numpy.ndarray, list[list[int]], int, dict[tuple[int,int], int], dict[int, list[int]]] : A tuple containing:
|
|
108
|
+
tuple[scipy.spatial.Voronoi, numpy.ndarray, list[list[int]], int, dict[tuple[int,int], int], dict[int, list[int]]] : A *tuple* containing:
|
|
97
109
|
|
|
98
110
|
- **vor**: SciPy Voronoi object for current points with extensions.
|
|
99
111
|
- **vertices_all**: (M,2) array of all Voronoi vertices including extensions.
|
|
100
|
-
- **ridge_vertices_all**:
|
|
112
|
+
- **ridge_vertices_all**: List of lists of vertex indices for each ridge, including extensions.
|
|
101
113
|
- **num_vertices**: Number of Voronoi vertices before adding extension.
|
|
102
|
-
- **vertexpair2ridge**: dict mapping vertex index pairs to ridge index.
|
|
103
|
-
- **vertex_points**: dict mapping vertex index to list of associated point indices.
|
|
114
|
+
- **vertexpair2ridge**: *dict* mapping vertex index pairs to ridge index.
|
|
115
|
+
- **vertex_points**: *dict* mapping vertex index to list of associated point indices.
|
|
104
116
|
"""
|
|
117
|
+
|
|
118
|
+
Voronoi = self._voronoi
|
|
119
|
+
|
|
105
120
|
r = self.phys.r
|
|
106
121
|
pts = self.pts
|
|
107
122
|
N = self.N
|
|
@@ -271,26 +286,26 @@ class FiniteVoronoiSimulator:
|
|
|
271
286
|
Args:
|
|
272
287
|
vor: SciPy Voronoi object for current points with extensions.
|
|
273
288
|
vertices_all: (M,2) array of all Voronoi vertices including extensions.
|
|
274
|
-
ridge_vertices_all:
|
|
289
|
+
ridge_vertices_all: (R,2) array of vertex indices for each ridge.
|
|
275
290
|
num_vertices: Number of Voronoi vertices before adding extension.
|
|
276
|
-
vertexpair2ridge: dict mapping vertex index pairs to ridge index.
|
|
291
|
+
vertexpair2ridge: *dict* mapping vertex index pairs to ridge index.
|
|
277
292
|
|
|
278
293
|
Returns:
|
|
279
294
|
dict[str, object]: A diagnostics dictionary containing:
|
|
280
295
|
|
|
281
|
-
- **vertex_in_id**: set of inner vertex ids.
|
|
282
|
-
- **vertex_out_id**: set of outer vertex ids.
|
|
296
|
+
- **vertex_in_id**: *set* of inner vertex ids.
|
|
297
|
+
- **vertex_out_id**: *set* of outer vertex ids.
|
|
283
298
|
- **vertices_out**: (L,2) array of outer vertex coordinates.
|
|
284
299
|
- **vertex_out_points**: (L,2) array of point index pairs associated with each outer vertex.
|
|
285
|
-
- **vertex_out_da_dtheta**:
|
|
286
|
-
- **vertex_out_dl_dtheta**:
|
|
287
|
-
- **dA_poly_dh**:
|
|
288
|
-
- **dP_poly_dh**:
|
|
289
|
-
- **area_list**:
|
|
290
|
-
- **perimeter_list**:
|
|
291
|
-
- **point_edges_type**:
|
|
292
|
-
- **point_vertices_f_idx**:
|
|
293
|
-
- **num_vertices_ext**:
|
|
300
|
+
- **vertex_out_da_dtheta**: Array of dA/dtheta for all outer vertices.
|
|
301
|
+
- **vertex_out_dl_dtheta**: Array of dL/dtheta for all outer vertices.
|
|
302
|
+
- **dA_poly_dh**: Array of dA_polygon/dh for each vertex.
|
|
303
|
+
- **dP_poly_dh**: Array of dP_polygon/dh for each vertex.
|
|
304
|
+
- **area_list**: Array of polygon areas for each cell.
|
|
305
|
+
- **perimeter_list**: Array of polygon perimeters for each cell.
|
|
306
|
+
- **point_edges_type**: List of lists of edge types per cell.
|
|
307
|
+
- **point_vertices_f_idx**: List of lists of vertex ids per cell.
|
|
308
|
+
- **num_vertices_ext**: Number of vertices including infinite extension vertices.
|
|
294
309
|
"""
|
|
295
310
|
N = self.N
|
|
296
311
|
r = self.phys.r
|
|
@@ -696,7 +711,7 @@ class FiniteVoronoiSimulator:
|
|
|
696
711
|
return F
|
|
697
712
|
|
|
698
713
|
# --------------------- One integration step ---------------------
|
|
699
|
-
def build(self) -> dict[str, object]:
|
|
714
|
+
def build(self, connect: bool = True) -> dict[str, object]:
|
|
700
715
|
""" Build the finite-Voronoi structure and compute forces, returning a dictionary of diagnostics.
|
|
701
716
|
|
|
702
717
|
Do the following:
|
|
@@ -705,6 +720,10 @@ class FiniteVoronoiSimulator:
|
|
|
705
720
|
- Compute per-cell quantities and derivatives
|
|
706
721
|
- Assemble forces
|
|
707
722
|
|
|
723
|
+
Args:
|
|
724
|
+
connect: Whether to compute cell connectivity information.
|
|
725
|
+
Setting this to ``False`` saves some computation time (though
|
|
726
|
+
very marginal) when connectivity is not needed.
|
|
708
727
|
|
|
709
728
|
Returns:
|
|
710
729
|
dict[str, object]: A dictionary containing geometric properties with keys:
|
|
@@ -713,14 +732,18 @@ class FiniteVoronoiSimulator:
|
|
|
713
732
|
- **areas**: (N,) array of cell areas
|
|
714
733
|
- **perimeters**: (N,) array of cell perimeters
|
|
715
734
|
- **vertices**: (M,2) array of all Voronoi + extension vertices
|
|
716
|
-
- **edges_type**:
|
|
717
|
-
- **regions**:
|
|
718
|
-
- **connections**: (
|
|
735
|
+
- **edges_type**: List-of-lists of edge types per cell (1=straight, 0=circular arc)
|
|
736
|
+
- **regions**: List-of-lists of vertex indices per cell
|
|
737
|
+
- **connections**: (K,2) array of connected cell index pairs
|
|
719
738
|
"""
|
|
720
739
|
(vor, vertices_all, ridge_vertices_all, num_vertices,
|
|
721
740
|
vertexpair2ridge, vertex_points) = self._build_voronoi_with_extensions()
|
|
722
741
|
|
|
723
|
-
|
|
742
|
+
# Get connectivity info
|
|
743
|
+
if connect:
|
|
744
|
+
connections = self._get_connections(vor.ridge_points, vertices_all, ridge_vertices_all)
|
|
745
|
+
else: # pragma: no cover
|
|
746
|
+
connections = np.empty((0,2), dtype=int)
|
|
724
747
|
|
|
725
748
|
geom, vertices_all = self._per_cell_geometry(vor, vertices_all, ridge_vertices_all, num_vertices, vertexpair2ridge)
|
|
726
749
|
|
|
@@ -750,11 +773,11 @@ class FiniteVoronoiSimulator:
|
|
|
750
773
|
)
|
|
751
774
|
|
|
752
775
|
# --------------------- 2D plotting utilities ---------------------
|
|
753
|
-
def plot_2d(self, ax: Axes | None = None, show: bool = False) -> Axes:
|
|
776
|
+
def plot_2d(self, ax: matplotlib.axes.Axes | None = None, show: bool = False) -> matplotlib.axes.Axes:
|
|
754
777
|
"""
|
|
755
778
|
Build the finite-Voronoi structure and render a 2D snapshot.
|
|
756
779
|
|
|
757
|
-
Basically a wrapper of :py:
|
|
780
|
+
Basically a wrapper of :py:meth:`_build_voronoi_with_extensions` and :py:meth:`_per_cell_geometry` functions + plot.
|
|
758
781
|
|
|
759
782
|
Args:
|
|
760
783
|
ax: If provided, draw into this axes; otherwise get the current axes.
|
|
@@ -768,6 +791,8 @@ class FiniteVoronoiSimulator:
|
|
|
768
791
|
|
|
769
792
|
geom, vertices_all = self._per_cell_geometry(vor, vertices_all, ridge_vertices_all, num_vertices, vertexpair2ridge)
|
|
770
793
|
|
|
794
|
+
from matplotlib import pyplot as plt
|
|
795
|
+
|
|
771
796
|
if ax is None:
|
|
772
797
|
ax = plt.gca()
|
|
773
798
|
|
|
@@ -779,7 +804,7 @@ class FiniteVoronoiSimulator:
|
|
|
779
804
|
return ax
|
|
780
805
|
|
|
781
806
|
# --------------------- Paradigm of plotting ---------------------
|
|
782
|
-
def _plot_routine(self, ax: Axes, vor: Voronoi, vertices_all: np.ndarray, ridge_vertices_all: list[list[int]],
|
|
807
|
+
def _plot_routine(self, ax: matplotlib.axes.Axes, vor: Voronoi, vertices_all: np.ndarray, ridge_vertices_all: list[list[int]],
|
|
783
808
|
point_edges_type: list[list[int]], point_vertices_f_idx: list[list[int]]) -> None:
|
|
784
809
|
"""
|
|
785
810
|
Low-level plot routine. Draws:
|
|
@@ -887,13 +912,12 @@ class FiniteVoronoiSimulator:
|
|
|
887
912
|
Update cell center positions.
|
|
888
913
|
|
|
889
914
|
.. note::
|
|
890
|
-
If the number of
|
|
891
|
-
are reset to the default value
|
|
892
|
-
|
|
893
|
-
*A0* argument.
|
|
915
|
+
If the number of cells changes, the preferred areas for all cells
|
|
916
|
+
are reset to the default value---defined either at simulator instantiation
|
|
917
|
+
or by :py:meth:`update_params`---unless *A0* is explicitly specified.
|
|
894
918
|
|
|
895
919
|
Args:
|
|
896
|
-
pts: New cell center positions.
|
|
920
|
+
pts : New cell center positions.
|
|
897
921
|
A0: Optional, set new preferred area(s).
|
|
898
922
|
|
|
899
923
|
Raises:
|
|
@@ -923,10 +947,10 @@ class FiniteVoronoiSimulator:
|
|
|
923
947
|
Update physical parameters.
|
|
924
948
|
|
|
925
949
|
Args:
|
|
926
|
-
phys: New PhysicalParams object.
|
|
950
|
+
phys: New :py:class:`PhysicalParams` object.
|
|
927
951
|
|
|
928
952
|
Raises:
|
|
929
|
-
TypeError: If *phys* is not an instance of PhysicalParams
|
|
953
|
+
TypeError: If *phys* is not an instance of :py:class:`PhysicalParams`.
|
|
930
954
|
|
|
931
955
|
.. warning::
|
|
932
956
|
This also resets all preferred cell areas to the new value of *A0*.
|
|
@@ -943,7 +967,8 @@ class FiniteVoronoiSimulator:
|
|
|
943
967
|
Update the preferred areas for all cells.
|
|
944
968
|
|
|
945
969
|
Args:
|
|
946
|
-
A0: New preferred area(s) for all cells
|
|
970
|
+
A0: New preferred area(s) for all cells, either as a scalar or
|
|
971
|
+
as an array of shape (N,).
|
|
947
972
|
|
|
948
973
|
Raises:
|
|
949
974
|
ValueError: If *A0* does not match cell number.
|
|
@@ -963,8 +988,8 @@ class FiniteVoronoiSimulator:
|
|
|
963
988
|
|
|
964
989
|
@property
|
|
965
990
|
def preferred_areas(self) -> np.ndarray:
|
|
966
|
-
"""
|
|
967
|
-
Preferred areas for all cells (read-only).
|
|
991
|
+
r"""
|
|
992
|
+
Preferred areas :math:`\{A_{0,i}\}` for all cells (read-only).
|
|
968
993
|
|
|
969
994
|
Returns:
|
|
970
995
|
numpy.ndarray: A copy of the internal preferred area array.
|
pyafv/physical_params.py
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import numpy as np
|
|
3
|
-
from scipy.optimize import minimize
|
|
4
3
|
from dataclasses import dataclass, replace
|
|
5
4
|
|
|
6
5
|
|
|
6
|
+
def _require_float_scalar(x: object, name: str) -> float: # pragma: no cover
|
|
7
|
+
"""
|
|
8
|
+
Accept Python real scalars (int/float) and NumPy real scalars.
|
|
9
|
+
Reject other types (including bool).
|
|
10
|
+
Return a normalized Python float.
|
|
11
|
+
"""
|
|
12
|
+
# Reject bool explicitly (since bool is a subclass of int)
|
|
13
|
+
if isinstance(x, bool):
|
|
14
|
+
raise TypeError(f"{name} must be a real scalar (float-like), got bool")
|
|
15
|
+
elif isinstance(x, (int, float, np.integer, np.floating)):
|
|
16
|
+
xf = float(x)
|
|
17
|
+
if not np.isfinite(xf):
|
|
18
|
+
raise ValueError(f"{name} must be finite, got {x}")
|
|
19
|
+
return xf
|
|
20
|
+
|
|
21
|
+
# Reject everything else
|
|
22
|
+
raise TypeError(f"{name} must be a real scalar (float-like), got {type(x).__name__}")
|
|
23
|
+
|
|
24
|
+
|
|
7
25
|
def sigmoid(x):
|
|
8
26
|
# stable sigmoid that handles large |x|
|
|
9
27
|
if x >= 0:
|
|
@@ -16,35 +34,55 @@ def sigmoid(x):
|
|
|
16
34
|
|
|
17
35
|
@dataclass(frozen=True)
|
|
18
36
|
class PhysicalParams:
|
|
19
|
-
"""Physical parameters for the active-finite-Voronoi (AFV) model.
|
|
37
|
+
r"""Physical parameters for the active-finite-Voronoi (AFV) model.
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
Frozen dataclass is used for :py:class:`PhysicalParams` to ensure immutability of instances.
|
|
39
|
+
.. warning::
|
|
40
|
+
* **Frozen dataclass** is used for :py:class:`PhysicalParams` to ensure immutability of instances.
|
|
41
|
+
* Do not set :py:attr:`delta` unless you know what you are doing.
|
|
23
42
|
|
|
24
43
|
Args:
|
|
25
|
-
r: Radius (maximal) of the Voronoi cells
|
|
44
|
+
r: Radius (maximal) of the Voronoi cells, sometimes denoted as :math:`\ell`.
|
|
26
45
|
A0: Preferred area of the Voronoi cells.
|
|
27
46
|
P0: Preferred perimeter of the Voronoi cells.
|
|
28
47
|
KA: Area elasticity constant.
|
|
29
48
|
KP: Perimeter elasticity constant.
|
|
30
49
|
lambda_tension: Tension difference between non-contacting edges and contacting edges.
|
|
31
|
-
delta:
|
|
50
|
+
delta: Contact truncation threshold to avoid singularities in computations; if None, set to 0.45*r.
|
|
32
51
|
"""
|
|
33
52
|
|
|
34
|
-
r: float = 1.0 #: Radius (maximal) of the Voronoi cells
|
|
53
|
+
r: float = 1.0 #: Radius (maximal) of the Voronoi cells, sometimes denoted as :math:`\ell`.
|
|
35
54
|
A0: float = np.pi #: Preferred area of the Voronoi cells.
|
|
36
55
|
P0: float = 4.8 #: Preferred perimeter of the Voronoi cells.
|
|
37
56
|
KA: float = 1.0 #: Area elasticity constant.
|
|
38
57
|
KP: float = 1.0 #: Perimeter elasticity constant.
|
|
39
58
|
lambda_tension: float = 0.2 #: Tension difference between non-contacting edges and contacting edges.
|
|
40
|
-
delta: float =
|
|
41
|
-
|
|
59
|
+
delta: float | None = None #: Contact truncation threshold to avoid singularities in computations.
|
|
60
|
+
|
|
61
|
+
def __post_init__(self):
|
|
62
|
+
# Normalize and validate required scalar floats
|
|
63
|
+
object.__setattr__(self, "r", _require_float_scalar(self.r, "r"))
|
|
64
|
+
object.__setattr__(self, "A0", _require_float_scalar(self.A0, "A0"))
|
|
65
|
+
object.__setattr__(self, "P0", _require_float_scalar(self.P0, "P0"))
|
|
66
|
+
object.__setattr__(self, "KA", _require_float_scalar(self.KA, "KA"))
|
|
67
|
+
object.__setattr__(self, "KP", _require_float_scalar(self.KP, "KP"))
|
|
68
|
+
object.__setattr__(self, "lambda_tension", _require_float_scalar(self.lambda_tension, "lambda_tension"))
|
|
69
|
+
|
|
70
|
+
if self.delta is None:
|
|
71
|
+
object.__setattr__(self, "delta", 0.45 * self.r)
|
|
72
|
+
else:
|
|
73
|
+
try:
|
|
74
|
+
object.__setattr__(self, "delta", _require_float_scalar(self.delta, "delta"))
|
|
75
|
+
except TypeError: # pragma: no cover
|
|
76
|
+
raise TypeError(f"delta must be a real scalar (float-like) or None, got {type(self.delta).__name__}") from None
|
|
42
77
|
|
|
43
78
|
def get_steady_state(self) -> tuple[float, float]:
|
|
44
|
-
r"""
|
|
45
|
-
|
|
79
|
+
r"""Search for the steady-state :math:`(\ell,d)` of a cell doublet for the given physical parameters (by minimizing total energy).
|
|
80
|
+
|
|
46
81
|
Returns:
|
|
47
|
-
Steady-state :math:`(\
|
|
82
|
+
Steady-state (optimal) :math:`(\ell_0,d_0)` values.
|
|
83
|
+
|
|
84
|
+
.. note::
|
|
85
|
+
:math:`\ell` is the maximal cell radius, and :math:`2d` is the cell-center distance of a doublet (rather than :math:`d`).
|
|
48
86
|
"""
|
|
49
87
|
params = [self.KA, self.KP, self.A0, self.P0, self.lambda_tension]
|
|
50
88
|
result = self._minimize_energy(params, restarts=10)
|
|
@@ -52,23 +90,30 @@ class PhysicalParams:
|
|
|
52
90
|
return l, d
|
|
53
91
|
|
|
54
92
|
def with_optimal_radius(self) -> PhysicalParams:
|
|
55
|
-
"""Returns a new instance with the radius updated to steady state.
|
|
93
|
+
r"""Returns a new instance of :py:class:`PhysicalParams` with the maximum radius :math:`\ell` (or :py:attr:`r`) updated to the steady state value :math:`\ell_0` of cell doublets.
|
|
94
|
+
Other parameters (except :py:attr:`delta`) remain unchanged.
|
|
56
95
|
|
|
96
|
+
Basically a wrapper around :py:meth:`get_steady_state` + creating a new instance.
|
|
97
|
+
|
|
57
98
|
Returns:
|
|
58
99
|
New instance with optimal radius.
|
|
100
|
+
|
|
101
|
+
.. important::
|
|
102
|
+
In the returned instance, the contact truncation threshold :py:attr:`delta` is set to 0.45*r by default.
|
|
59
103
|
"""
|
|
60
104
|
l, d = self.get_steady_state()
|
|
61
|
-
new_params = replace(self, r=l)
|
|
105
|
+
new_params = replace(self, r=l, delta=0.45*l)
|
|
62
106
|
return new_params
|
|
63
107
|
|
|
64
108
|
def with_delta(self, delta_new: float) -> PhysicalParams:
|
|
65
|
-
"""Returns a new instance with the
|
|
66
|
-
|
|
109
|
+
"""Returns a new instance of :py:class:`PhysicalParams` with the new contact truncation threshold *delta_new*.
|
|
110
|
+
Other parameters remain unchanged.
|
|
111
|
+
|
|
67
112
|
Args:
|
|
68
|
-
delta_new: New delta value.
|
|
113
|
+
delta_new: New :py:attr:`delta` value.
|
|
69
114
|
|
|
70
115
|
Returns:
|
|
71
|
-
New instance with updated delta.
|
|
116
|
+
New instance with the updated delta value.
|
|
72
117
|
"""
|
|
73
118
|
return replace(self, delta=delta_new)
|
|
74
119
|
|
|
@@ -86,6 +131,9 @@ class PhysicalParams:
|
|
|
86
131
|
return KA * (A - A0)**2 + KP * (P - P0)**2 + Lambda * ln
|
|
87
132
|
|
|
88
133
|
def _minimize_energy(self, params, restarts=10, seed=None):
|
|
134
|
+
|
|
135
|
+
from scipy.optimize import minimize
|
|
136
|
+
|
|
89
137
|
rng = np.random.default_rng(seed)
|
|
90
138
|
best = None
|
|
91
139
|
for _ in range(restarts):
|
|
@@ -117,34 +165,56 @@ def target_delta(params: PhysicalParams, target_force: float) -> float:
|
|
|
117
165
|
params: Physical parameters of the AFV model.
|
|
118
166
|
target_force: Target detachment force.
|
|
119
167
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
KP, A0, P0, Lambda = params.KP, params.A0, params.P0, params.lambda_tension
|
|
124
|
-
l = params.r
|
|
168
|
+
Raises:
|
|
169
|
+
TypeError: If *params* is not an instance of :py:class:`PhysicalParams`.
|
|
170
|
+
ValueError: If the target force is not within the achievable range.
|
|
125
171
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for distance in distances:
|
|
129
|
-
epsilon = l - (distance/2.)
|
|
172
|
+
Returns:
|
|
173
|
+
Corresponding value of the truncation threshold :math:`\delta`.
|
|
130
174
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
175
|
+
.. note::
|
|
176
|
+
We search for the cell-cell separation at which the intercellular force
|
|
177
|
+
equals the target force, scanning distances from :math:`10^{-6}\ell` to
|
|
178
|
+
:math:`(2-10^{-6})\ell` in steps of :math:`10^{-6}\ell`, and select the
|
|
179
|
+
**largest distance** at which the match occurs.
|
|
180
|
+
"""
|
|
135
181
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
detachment_forces.append(f)
|
|
182
|
+
if not isinstance(params, PhysicalParams): # pragma: no cover
|
|
183
|
+
raise TypeError("params must be an instance of PhysicalParams")
|
|
139
184
|
|
|
140
|
-
|
|
185
|
+
KA, KP, A0, P0, Lambda = params.KA, params.KP, params.A0, params.P0, params.lambda_tension
|
|
186
|
+
l = params.r
|
|
141
187
|
|
|
142
|
-
|
|
188
|
+
distances = np.linspace(1e-6, 2.-(1e-6), 10**6) * l
|
|
189
|
+
|
|
190
|
+
epsilon = l - (distances/2.)
|
|
143
191
|
|
|
144
|
-
|
|
145
|
-
|
|
192
|
+
theta = 2 * np.pi - 2 * np.arctan2(np.sqrt(l**2 - (l - epsilon)**2), l - epsilon)
|
|
193
|
+
A = (l - epsilon) * np.sqrt(l**2 -
|
|
194
|
+
(l - epsilon)**2) + 0.5 * (l**2 * theta)
|
|
195
|
+
P = 2 * np.sqrt(l**2 - (l - epsilon)**2) + l * theta
|
|
146
196
|
|
|
147
|
-
|
|
197
|
+
detachment_forces = 4. * np.sqrt((2 * l - epsilon) * epsilon) * (KA * (A - A0) + KP * ((P - P0)/(2 * l - epsilon))
|
|
198
|
+
+ (Lambda/2) * l /((2 * l - epsilon) * epsilon))
|
|
199
|
+
|
|
200
|
+
# idx = np.abs(detachment_forces[None, :] - target_force).argmin()
|
|
201
|
+
# target_distances = distances[idx]
|
|
202
|
+
|
|
203
|
+
# ---------------------- Better way to search foot ----------------------------
|
|
204
|
+
f = detachment_forces - target_force # find root of f=0
|
|
205
|
+
cross = (f[:-1] == 0) | (np.signbit(f[:-1]) != np.signbit(f[1:])) # crossing points
|
|
206
|
+
|
|
207
|
+
if np.any(cross):
|
|
208
|
+
i = np.flatnonzero(cross)[-1] # last crossing interval [i, i+1]
|
|
209
|
+
# optional: linear interpolation for a better distance estimate
|
|
210
|
+
x0, x1 = distances[i], distances[i+1]
|
|
211
|
+
f0, f1 = f[i], f[i+1]
|
|
212
|
+
target_distance = x0 if f1 == f0 else x0 + (0 - f0) * (x1 - x0) / (f1 - f0)
|
|
213
|
+
else:
|
|
214
|
+
raise ValueError("No valid delta found for the given target force.")
|
|
215
|
+
# ------------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
delta = np.sqrt(4*(l**2) - target_distance**2)
|
|
148
218
|
|
|
149
219
|
return delta
|
|
150
220
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyafv
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Python implementation of the active-finite-Voronoi (AFV) model
|
|
5
|
+
Project-URL: Homepage, https://pyafv.github.io
|
|
6
|
+
Project-URL: Download, https://pypi.org/project/pyafv/#files
|
|
7
|
+
Project-URL: Source Code, https://github.com/wwang721/pyafv
|
|
8
|
+
Project-URL: Documentation, https://pyafv.readthedocs.io/
|
|
9
|
+
Project-URL: Changelog, https://github.com/wwang721/pyafv/releases/latest
|
|
10
|
+
Author: Wei Wang
|
|
11
|
+
Author-email: ww000721@gmail.com
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: biological-modeling,cellular-patterns,voronoi-model
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering
|
|
26
|
+
Requires-Python: <3.15,>=3.10
|
|
27
|
+
Requires-Dist: matplotlib>=3.8.4
|
|
28
|
+
Requires-Dist: numpy>=1.26.4
|
|
29
|
+
Requires-Dist: scipy>=1.13.1
|
|
30
|
+
Provides-Extra: examples
|
|
31
|
+
Requires-Dist: ipywidgets>=8.1.5; extra == 'examples'
|
|
32
|
+
Requires-Dist: jupyter>=1.1.0; extra == 'examples'
|
|
33
|
+
Requires-Dist: tqdm>=4.67.1; extra == 'examples'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/pyafv/)
|
|
37
|
+
[](https://pypi.org/project/pyafv/)
|
|
38
|
+
[](https://pyafv.readthedocs.io)
|
|
39
|
+
<!--[](https://github.com/wwang721/pyafv/actions/workflows/tests.yml?query=branch:main)-->
|
|
40
|
+
[](https://github.com/wwang721/pyafv/actions/workflows/tests_all_platform.yml)
|
|
41
|
+
[](https://github.com/wwang721/pyafv/actions/workflows/tests.yml)
|
|
42
|
+
[](https://codecov.io/github/wwang721/pyafv/tree/main)
|
|
43
|
+
[](https://opensource.org/licenses/MIT)
|
|
44
|
+
|
|
45
|
+
<!--
|
|
46
|
+
[](https://doi.org/10.48550/arXiv.2503.03126)
|
|
47
|
+
[](https://doi.org/10.1103/PhysRevE.109.054408)
|
|
48
|
+
[](https://doi.org/10.1103/PhysRevE.109.054408)
|
|
49
|
+
-->
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# PyAFV
|
|
53
|
+
|
|
54
|
+
Python code that implements the **active-finite-Voronoi (AFV) model** in 2D.
|
|
55
|
+
The AFV framework was introduced and developed in, for example, Refs. [[1](#huang2023bridging)–[3](#wang2026divergence)].
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
**PyAFV** is available on **PyPI** and can be installed using *pip* directly:
|
|
61
|
+
```bash
|
|
62
|
+
pip install pyafv
|
|
63
|
+
```
|
|
64
|
+
The package supports Python ≥ 3.10 and < 3.15, including Python 3.14t (the free-threaded, no-GIL build).
|
|
65
|
+
To verify that the installation was successful and that the correct version is installed, run the following in Python:
|
|
66
|
+
```python
|
|
67
|
+
import pyafv
|
|
68
|
+
print(pyafv.__version__)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> On HPC clusters, global Python path can contaminate the runtime environment. You may need to clear it explicitly using `unset PYTHONPATH` or prefixing the *pip* command with `PYTHONPATH=""`.
|
|
72
|
+
|
|
73
|
+
### Install from source
|
|
74
|
+
|
|
75
|
+
Installing from source can be necessary if *pip* installation does not work. First, download and unzip the source code, then navigate to the root directory of the package and run:
|
|
76
|
+
```bash
|
|
77
|
+
pip install .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> **Note:** A C/C++ compiler is required if you are building from source, since some components of **PyAFV** are implemented in Cython for performance optimization.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
#### Windows MinGW GCC
|
|
84
|
+
|
|
85
|
+
If you are using **MinGW GCC** (rather than **MSVC**) on *Windows*, to build from the source code, add a `setup.cfg` at the repository root before running `pip install .` with the following content:
|
|
86
|
+
```ini
|
|
87
|
+
# setup.cfg
|
|
88
|
+
[build_ext]
|
|
89
|
+
compiler=mingw32
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
### Install offline
|
|
94
|
+
|
|
95
|
+
If you need to install **PyAFV** on a machine without internet access, you can download the corresponding wheel file from **PyPI** and transfer it to the target machine, and then run the following command to install using *pip*:
|
|
96
|
+
```bash
|
|
97
|
+
pip install pyafv-<version>-<platform>.whl
|
|
98
|
+
```
|
|
99
|
+
Alternatively, you can build **PyAFV** from source as described in the previous section. In this case, in addition to the required prerequisites of the package, the build-time dependencies **hatchling** and **hatch-cython** must also be available.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Usage
|
|
103
|
+
|
|
104
|
+
Here is a simple example to get you started, demonstrating how to construct a finite-Voronoi diagram:
|
|
105
|
+
```python
|
|
106
|
+
import numpy as np
|
|
107
|
+
import pyafv as afv
|
|
108
|
+
|
|
109
|
+
N = 100 # number of cells
|
|
110
|
+
pts = np.random.rand(N, 2) * 10 # initial positions
|
|
111
|
+
params = afv.PhysicalParams() # use default parameter values
|
|
112
|
+
sim = afv.FiniteVoronoiSimulator(pts, params) # initialize the simulator
|
|
113
|
+
sim.plot_2d(show=True) # visualize the Voronoi diagram
|
|
114
|
+
```
|
|
115
|
+
To compute the conservative forces and extract detailed geometric information (e.g., cell areas, vertices, and edges), call:
|
|
116
|
+
```python
|
|
117
|
+
diag = sim.build()
|
|
118
|
+
```
|
|
119
|
+
The returned object `diag` is a Python `dict` containing these quantities.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Simulation previews
|
|
123
|
+
|
|
124
|
+
Below are representative simulation snapshots generated using the code:
|
|
125
|
+
|
|
126
|
+
| Model illustration | Periodic boundary conditions |
|
|
127
|
+
|-----------------|-----------------|
|
|
128
|
+
| <img src="https://media.githubusercontent.com/media/wwang721/pyafv/main/assets/model_illustration.png" width="540"> | <img src="https://media.githubusercontent.com/media/wwang721/pyafv/main/assets/pbc.png" width="385">|
|
|
129
|
+
|
|
130
|
+
| Initial configuration | After relaxation | Active dynamics enabled |
|
|
131
|
+
|-----------------------|-----------------------|-----------------------|
|
|
132
|
+
| <img src="https://media.githubusercontent.com/media/wwang721/pyafv/main/assets/initial_configuration.png" width="300"> | <img src="https://media.githubusercontent.com/media/wwang721/pyafv/main/assets/relaxed_configuration.png" width="300"> | <img src="https://media.githubusercontent.com/media/wwang721/pyafv/main/assets/active_FV.png" width="300"> |
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
## More information
|
|
136
|
+
|
|
137
|
+
- **Full documentation** on [readthedocs](https://pyafv.readthedocs.io) or as [a single PDF file](https://pyafv.readthedocs.io/_/downloads/en/latest/pdf/).
|
|
138
|
+
|
|
139
|
+
- See [CONTRIBUTING.md](https://github.com/wwang721/pyafv/blob/main/.github/CONTRIBUTING.md) or the [documentation](https://pyafv.readthedocs.io/latest/contributing.html) for **local development instructions**.
|
|
140
|
+
|
|
141
|
+
- See some important [**issues**](https://github.com/wwang721/pyafv/issues?q=is%3Aissue%20state%3Aclosed) for additional context, such as:
|
|
142
|
+
* [QhullError when 3+ points are collinear #1](https://github.com/wwang721/pyafv/issues/1) [Closed - see [comments](https://github.com/wwang721/pyafv/issues/1#issuecomment-3701355742)]
|
|
143
|
+
* [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)]
|
|
144
|
+
* [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)]
|
|
145
|
+
|
|
146
|
+
- Some releases of this repository are cross-listed on [Zenodo](https://doi.org/10.5281/zenodo.18091659):
|
|
147
|
+
|
|
148
|
+
[](https://doi.org/10.5281/zenodo.18091659)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## References
|
|
152
|
+
|
|
153
|
+
<table>
|
|
154
|
+
<tr>
|
|
155
|
+
<td id="huang2023bridging" valign="top">[1]</td>
|
|
156
|
+
<td>
|
|
157
|
+
J. Huang, H. Levine, and D. Bi, <em>Bridging the gap between collective motility and epithelial-mesenchymal transitions through the active finite Voronoi model</em>, <a href="https://doi.org/10.1039/D3SM00327B">Soft Matter <strong>19</strong>, 9389 (2023)</a>.
|
|
158
|
+
</td>
|
|
159
|
+
</tr>
|
|
160
|
+
<tr>
|
|
161
|
+
<td id="teomy2018confluent" valign="top">[2]</td>
|
|
162
|
+
<td>
|
|
163
|
+
E. Teomy, D. A. Kessler, and H. Levine, <em>Confluent and nonconfluent phases in a model of cell tissue</em>, <a href="https://doi.org/10.1103/PhysRevE.98.042418">Phys. Rev. E <strong>98</strong>, 042418 (2018)</a>.
|
|
164
|
+
</td>
|
|
165
|
+
</tr>
|
|
166
|
+
<tr>
|
|
167
|
+
<td id="wang2026divergence" valign="top">[3]</td>
|
|
168
|
+
<td>
|
|
169
|
+
W. Wang (汪巍) and B. A. Camley, <em>Divergence of detachment forces in the finite-Voronoi model</em>, manuscript in preparation (2026).
|
|
170
|
+
</td>
|
|
171
|
+
</tr>
|
|
172
|
+
</table>
|