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.
@@ -12,16 +12,19 @@ Key public entry points:
12
12
  - update_params(): update physical parameters.
13
13
  """
14
14
 
15
- from typing import Dict, List, Tuple, Optional
16
- import numpy as np
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
- from .physical_params import PhysicalParams
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
- 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
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
- def __init__(self, pts: np.ndarray, phys: PhysicalParams):
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) -> Tuple[Voronoi, np.ndarray, List[List[int]], int, Dict[Tuple[int,int], int], Dict[int, List[int]]]:
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 SciPy Voronoi for current points. For N<=2, emulate regions.
43
- For N>=3, extend infinite ridges by adding long rays and update
44
- regions accordingly. Return augmented structures.
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 = build_vertexpair_and_vertexpoints_cy(ridge_vertices_all, vor.ridge_points, num_vertices, N)
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: Dict[Tuple[int, int], int]) -> Dict:
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
- Iterate cells to:
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 'outer' vertices created at arc intersections and track their point pairs
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
- A0 = self.phys.A0
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 = pad_regions_cy(vor.regions) # (R, Kmax) int64 with -1 padding
343
- point_edges_type, point_vertices_f_idx = build_point_edges_cy(
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 = compute_vertex_derivatives_cy(
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
- float(A0),
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: Dict[int, List[int]], vertex_in_id: List[int], vertex_out_id: List[int],
387
- vertex_out_points: List[List[int]], vertex_out_da_dtheta: np.ndarray,
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
- A0 = self.phys.A0
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] - A0) * v_da[:, 0]
573
- Aj_w_j = (area_list[J] - A0) * v_da[:, 1]
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) -> Dict:
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
- Returns a dictionary of diagnostics.
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: Optional[Axes] = None, show: bool = False) -> Axes:
765
+ def plot_2d(self, ax: matplotlib.axes.Axes | None = None, show: bool = False) -> matplotlib.axes.Axes:
654
766
  """
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
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: List[List[int]],
685
- point_edges_type: List[List[int]], point_vertices_f_idx: List[List[int]]) -> None:
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: List[List[int]], vertices_all: np.ndarray, ridge_vertices_all: List[List[int]]) -> np.ndarray:
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: np.ndarray) -> None:
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
- self.N, dim = pts.shape
792
- if dim != 2:
793
- raise ValueError("Positions must have shape (N,2)")
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
- # Radius (maximal) of the Voronoi cells
19
- 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
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
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.9
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
  [![Downloads](https://img.shields.io/pypi/dm/pyafv.svg)](https://pypi.org/project/pyafv/)
36
37
  [![Zenodo](https://zenodo.org/badge/1124385738.svg)](https://doi.org/10.5281/zenodo.18091659)
37
38
  <!--[![pytest](https://github.com/wwang721/pyafv/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/wwang721/pyafv/actions/workflows/tests.yml?query=branch:main)-->
39
+ [![Tests on all platforms](https://github.com/wwang721/pyafv/actions/workflows/tests_all_platform.yml/badge.svg)](https://github.com/wwang721/pyafv/actions/workflows/tests_all_platform.yml)
38
40
  [![pytest](https://github.com/wwang721/pyafv/actions/workflows/tests.yml/badge.svg)](https://github.com/wwang721/pyafv/actions/workflows/tests.yml)
39
41
  [![codecov](https://codecov.io/github/wwang721/pyafv/branch/main/graph/badge.svg?token=VSXSOX8HVS)](https://codecov.io/github/wwang721/pyafv/tree/main)
40
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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
- Install **PyAFV** with:
53
+ To install **PyAFV** with `pip`, run:
52
54
  ```bash
53
55
  pip install pyafv
54
56
  ```
55
- This package requires Python ≥ 3.9 and < 3.15.
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,,