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 CHANGED
@@ -1,12 +1,30 @@
1
- from .physical_params import PhysicalParams, target_delta
2
- from .simulator import FiniteVoronoiSimulator
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
- from importlib.metadata import version, PackageNotFoundError
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
- __version__ = version("pyafv")
8
- except PackageNotFoundError:
9
- __version__ = "unknown" # package is not installed
24
+ from ._version import __version__
25
+ except ImportError:
26
+ __version__ = "unknown"
27
+
10
28
 
11
29
  __all__ = [
12
30
  "PhysicalParams",
pyafv/_version.py ADDED
@@ -0,0 +1,2 @@
1
+ # pyafv/_version.py
2
+ __version__ = "0.3.4"
pyafv/backend.py CHANGED
@@ -1,16 +1,20 @@
1
1
  # chooses fast vs fallback implementation
2
2
 
3
3
  try:
4
- from . import finite_voronoi_fast as _impl
4
+ from . import cell_geom as _impl
5
5
  _BACKEND_NAME = "cython"
6
- except ImportError: # pragma: no cover
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
- from . import finite_voronoi_fallback as _impl
10
+ _IMPORT_ERROR = e
11
+
9
12
 
10
13
  # ---- for explicit API ----
11
- backend_simulator = _impl.FiniteVoronoiSimulator
14
+ backend_impl = _impl
12
15
 
13
16
  __all__ = [
14
- "backend_simulator",
17
+ "backend_impl",
15
18
  "_BACKEND_NAME",
19
+ "_IMPORT_ERROR"
16
20
  ]
@@ -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
- def __init__(self, pts: np.ndarray, phys: PhysicalParams):
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 = pts.shape[0] # Number of points
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 = build_vertexpair_and_vertexpoints_cy(ridge_vertices_all, vor.ridge_points, num_vertices, N)
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 = pad_regions_cy(vor.regions) # (R, Kmax) int64 with -1 padding
343
- point_edges_type, point_vertices_f_idx = build_point_edges_cy(
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 = compute_vertex_derivatives_cy(
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) -> Dict:
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
- Returns a dictionary of diagnostics.
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(+extensions) and render a 2D snapshot.
656
-
657
- Parameters
658
- ----------
659
- ax : matplotlib.axes.Axes or None
660
- If provided, draw into this axes; otherwise get the current axes.
661
- show : bool
662
- Whether to call plt.show() at the end.
663
-
664
- Returns
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
- self.N, dim = pts.shape
850
+ pts = np.asarray(pts, dtype=float)
851
+ N, dim = pts.shape
792
852
  if dim != 2:
793
- raise ValueError("Positions must have shape (N,2)")
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
- # Radius (maximal) of the Voronoi cells
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
- A0: float = np.pi # Preferred area of the Voronoi cells
21
- P0: float = 4.8 # Preferred perimeter of the Voronoi cells
22
- KA: float = 1.0 # Area elasticity
23
- KP: float = 1.0 # Perimeter elasticity
24
- lambda_tension: float = 0.2 # Tension difference
25
- delta: float = 0. # Small offset to avoid singularities
26
-
27
- def get_steady_state(self):
28
- # compute steady-state (l,d) given physical params
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
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
- Install **PyAFV** with:
51
+ To install **PyAFV** with `pip`, run:
52
52
  ```bash
53
53
  pip install pyafv
54
54
  ```
55
- This package requires Python ≥ 3.9 and < 3.15.
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