pyafv 0.3.3__cp39-cp39-macosx_10_9_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 +15 -0
- pyafv/backend.py +16 -0
- pyafv/cell_geom.cpython-39-darwin.so +0 -0
- pyafv/finite_voronoi_fallback.py +989 -0
- pyafv/finite_voronoi_fast.py +802 -0
- pyafv/physical_params.py +117 -0
- pyafv/simulator.py +37 -0
- pyafv-0.3.3.dist-info/METADATA +110 -0
- pyafv-0.3.3.dist-info/RECORD +12 -0
- pyafv-0.3.3.dist-info/WHEEL +6 -0
- pyafv-0.3.3.dist-info/licenses/LICENSE +21 -0
- pyafv-0.3.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Finite Voronoi Model Simulator in 2D
|
|
3
|
+
====================================
|
|
4
|
+
Created by Wei Wang, 2025.
|
|
5
|
+
|
|
6
|
+
Key public entry points:
|
|
7
|
+
------------------------
|
|
8
|
+
- FiniteVoronoiSimulator: configure parameters.
|
|
9
|
+
- build(): build the finite Voronoi diagram and compute forces.
|
|
10
|
+
- plot_2d(): plot the finite Voronoi diagram with matplotlib.
|
|
11
|
+
- update_points(): update cell center positions.
|
|
12
|
+
- update_params(): update physical parameters.
|
|
13
|
+
"""
|
|
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
|
|
20
|
+
|
|
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
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---- tiny helpers to avoid tiny allocations in hot loops ----
|
|
28
|
+
def _row_dot(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
29
|
+
"""Row-wise dot product for 2D arrays with shape (N,2)."""
|
|
30
|
+
return np.einsum("ij,ij->i", a, b)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FiniteVoronoiSimulator:
|
|
34
|
+
def __init__(self, pts: np.ndarray, phys: PhysicalParams):
|
|
35
|
+
self.pts = pts.copy() # (N,2) array of initial points
|
|
36
|
+
self.N = pts.shape[0] # Number of points
|
|
37
|
+
self.phys = phys
|
|
38
|
+
|
|
39
|
+
# --------------------- 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]]]:
|
|
41
|
+
"""
|
|
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.
|
|
45
|
+
"""
|
|
46
|
+
r = self.phys.r
|
|
47
|
+
pts = self.pts
|
|
48
|
+
N = self.N
|
|
49
|
+
|
|
50
|
+
# Special handling: N == 1, 2
|
|
51
|
+
if N == 1:
|
|
52
|
+
vor = Voronoi(np.random.rand(3, 2))
|
|
53
|
+
vor.points[:N] = pts
|
|
54
|
+
vor.vertices = np.array([]).reshape(-1, 2)
|
|
55
|
+
|
|
56
|
+
vor.regions = [[]]
|
|
57
|
+
vor.ridge_vertices = np.array([]).reshape(-1, 2)
|
|
58
|
+
vor.point_region = np.array([0])
|
|
59
|
+
vor.ridge_points = np.array([]).reshape(-1, 2)
|
|
60
|
+
|
|
61
|
+
vertices_all = vor.vertices
|
|
62
|
+
ridge_vertices_all = vor.ridge_vertices
|
|
63
|
+
num_vertices = len(vor.vertices)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if N == 2:
|
|
67
|
+
vor = Voronoi(np.random.rand(3, 2))
|
|
68
|
+
vor.points[:N] = pts
|
|
69
|
+
|
|
70
|
+
p1, p2 = pts
|
|
71
|
+
center = (p1 + p2) / 2.0
|
|
72
|
+
|
|
73
|
+
t = p1 - p2
|
|
74
|
+
t_norm = np.linalg.norm(t)
|
|
75
|
+
t /= t_norm
|
|
76
|
+
|
|
77
|
+
n = np.array([-t[1], t[0]]) # Perpendicular vector of t
|
|
78
|
+
|
|
79
|
+
if t_norm >= 2 * r:
|
|
80
|
+
vor.vertices = np.array([]).reshape(-1, 2)
|
|
81
|
+
vor.regions = [[], []]
|
|
82
|
+
vor.ridge_vertices = np.array([]).reshape(-1, 2)
|
|
83
|
+
vor.ridge_points = np.array([]).reshape(-1, 2)
|
|
84
|
+
else:
|
|
85
|
+
v1 = center + 2 * r * n
|
|
86
|
+
v2 = center - 2 * r * n
|
|
87
|
+
v3 = center + 3 * r * (p1 - center) / np.linalg.norm(p2 - center)
|
|
88
|
+
v4 = center + 3 * r * (p2 - center) / np.linalg.norm(p2 - center)
|
|
89
|
+
vor.vertices = np.array([v1, v2, v3, v4]).reshape(-1, 2)
|
|
90
|
+
vor.ridge_vertices = np.array([[0, 1], [1, 2], [0, 2], [0, 3], [1, 3]])
|
|
91
|
+
vor.regions = [[0, 1, 2], [0, 1, 3]]
|
|
92
|
+
vor.ridge_points = np.array([[0, 1], [-1, 0], [-1, 0], [-1, 1], [-1, 1]])
|
|
93
|
+
|
|
94
|
+
vor.point_region = np.array([0, 1])
|
|
95
|
+
|
|
96
|
+
vertices_all = vor.vertices
|
|
97
|
+
ridge_vertices_all = vor.ridge_vertices
|
|
98
|
+
num_vertices = len(vor.vertices)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# N >= 3 (vectorized main path)
|
|
102
|
+
if N >= 3:
|
|
103
|
+
vor = Voronoi(pts)
|
|
104
|
+
"""
|
|
105
|
+
Basic info from Voronoi object:
|
|
106
|
+
-------------------------------
|
|
107
|
+
vor.vertices # (K,2) Voronoi vertices (finite)
|
|
108
|
+
vor.ridge_points # (R,2) pairs of input point indices sharing a Voronoi ridge
|
|
109
|
+
vor.ridge_vertices # list of vertex-index lists (may contain -1 for infinity)
|
|
110
|
+
vor.point_region # for each input point, the region index
|
|
111
|
+
vor.regions # list of regions; each is a list of vertex indices (may include -1)
|
|
112
|
+
"""
|
|
113
|
+
center = np.mean(pts, axis=0)
|
|
114
|
+
|
|
115
|
+
span_x = np.ptp(vor.vertices[:, 0]) # span in x
|
|
116
|
+
span_y = np.ptp(vor.vertices[:, 1]) # span in y
|
|
117
|
+
pts_span_x = np.ptp(pts[:, 0]) # span in x
|
|
118
|
+
pts_span_y = np.ptp(pts[:, 1]) # span in y
|
|
119
|
+
span = max(span_x, span_y, pts_span_x, pts_span_y, 10. * r) # overall span
|
|
120
|
+
|
|
121
|
+
# Base copies
|
|
122
|
+
vertices_base = vor.vertices # (K,2)
|
|
123
|
+
rv_arr = np.asarray(vor.ridge_vertices, dtype=int) # (R,2) may contain -1
|
|
124
|
+
rp_arr = np.asarray(vor.ridge_points, dtype=int) # (R,2)
|
|
125
|
+
|
|
126
|
+
# Remove -1 from regions (we will append extension ids later)
|
|
127
|
+
vor.regions = [[vid for vid in region if vid >= 0] for region in vor.regions]
|
|
128
|
+
|
|
129
|
+
# Identify ridges with an infinite endpoint
|
|
130
|
+
inf_mask = (rv_arr == -1).any(axis=1) # (R,)
|
|
131
|
+
num_inf = int(inf_mask.sum())
|
|
132
|
+
|
|
133
|
+
if num_inf > 0:
|
|
134
|
+
rv_inf = rv_arr[inf_mask] # (M,2)
|
|
135
|
+
rp_inf = rp_arr[inf_mask] # (M,2)
|
|
136
|
+
|
|
137
|
+
# finite endpoint index per infinite ridge
|
|
138
|
+
v_idx_finite = np.where(rv_inf[:, 0] != -1, rv_inf[:, 0], rv_inf[:, 1]) # (M,)
|
|
139
|
+
|
|
140
|
+
# geometry for normals
|
|
141
|
+
p1 = pts[rp_inf[:, 0]] # (M,2)
|
|
142
|
+
p2 = pts[rp_inf[:, 1]] # (M,2)
|
|
143
|
+
mid = (p1 + p2) / 2.0 # (M,2)
|
|
144
|
+
|
|
145
|
+
t = p1 - p2 # (M,2)
|
|
146
|
+
t_norm = np.linalg.norm(t, axis=1, keepdims=True) # (M,1)
|
|
147
|
+
t_unit = t / t_norm
|
|
148
|
+
|
|
149
|
+
# (M,2), perpendicular
|
|
150
|
+
n = np.column_stack([-t_unit[:, 1], t_unit[:, 0]])
|
|
151
|
+
|
|
152
|
+
# Ensure "outward" normal
|
|
153
|
+
sign = np.einsum("ij,ij->i", (mid - center), n) # (M,)
|
|
154
|
+
n[sign < 0] *= -1.0
|
|
155
|
+
|
|
156
|
+
# Build extension points
|
|
157
|
+
ext = vertices_base[v_idx_finite] + (100.0 * span) * n # (M,2), long rays (extension must be long enough!)
|
|
158
|
+
|
|
159
|
+
# Concatenate once
|
|
160
|
+
K = vertices_base.shape[0]
|
|
161
|
+
vertices_all = np.vstack([vertices_base, ext]) # (K+M,2)
|
|
162
|
+
|
|
163
|
+
# New vertex ids for extensions
|
|
164
|
+
ext_ids = np.arange(K, K + num_inf, dtype=int) # (M,)
|
|
165
|
+
|
|
166
|
+
# Replace -1 with ext_ids in a vectorized way
|
|
167
|
+
rv_new = rv_arr.copy() # (R,2)
|
|
168
|
+
rv_sub = rv_new[inf_mask] # view (M,2)
|
|
169
|
+
|
|
170
|
+
pos0 = (rv_sub[:, 0] == -1) # (M,)
|
|
171
|
+
rv_sub[pos0, 0] = ext_ids[pos0]
|
|
172
|
+
rv_sub[~pos0, 1] = ext_ids[~pos0]
|
|
173
|
+
rv_new[inf_mask] = rv_sub
|
|
174
|
+
|
|
175
|
+
ridge_vertices_all = rv_new.tolist()
|
|
176
|
+
|
|
177
|
+
# Append extension id to both adjacent regions (list-of-lists => tiny loop)
|
|
178
|
+
for m in range(num_inf):
|
|
179
|
+
i1, i2 = rp_inf[m]
|
|
180
|
+
e = ext_ids[m]
|
|
181
|
+
region_id = vor.point_region[i1]
|
|
182
|
+
vor.regions[region_id].append(e)
|
|
183
|
+
region_id = vor.point_region[i2]
|
|
184
|
+
vor.regions[region_id].append(e)
|
|
185
|
+
else:
|
|
186
|
+
vertices_all = vertices_base.copy()
|
|
187
|
+
ridge_vertices_all = rv_arr.tolist()
|
|
188
|
+
|
|
189
|
+
# number of native (finite) vertices
|
|
190
|
+
num_vertices = len(vor.vertices)
|
|
191
|
+
|
|
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)
|
|
194
|
+
|
|
195
|
+
return vor, vertices_all, ridge_vertices_all, num_vertices, vertexpair2ridge, vertex_points
|
|
196
|
+
|
|
197
|
+
# --------------------- 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:
|
|
199
|
+
"""
|
|
200
|
+
Iterate cells to:
|
|
201
|
+
- sort polygon/arc vertices around each cell
|
|
202
|
+
- classify edges (1 = straight Voronoi edge; 0 = circular arc)
|
|
203
|
+
- compute area/perimeter for each cell
|
|
204
|
+
- 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
|
|
206
|
+
"""
|
|
207
|
+
N = self.N
|
|
208
|
+
r = self.phys.r
|
|
209
|
+
A0 = self.phys.A0
|
|
210
|
+
P0 = self.phys.P0
|
|
211
|
+
pts = self.pts
|
|
212
|
+
|
|
213
|
+
num_vertices_ext = len(vertices_all) # number of vertices with infinite extension points
|
|
214
|
+
|
|
215
|
+
rv = np.asarray(ridge_vertices_all, dtype=int) # (R,2)
|
|
216
|
+
rp = np.asarray(vor.ridge_points, dtype=int) # (R,2)
|
|
217
|
+
num_ridges = rp.shape[0]
|
|
218
|
+
|
|
219
|
+
# init outer-vertex arrays (same shapes you used)
|
|
220
|
+
vertices_out = np.zeros((2 * num_ridges, 2), dtype=float)
|
|
221
|
+
vertex_out_points = np.zeros((2 * num_ridges, 2), dtype=int)
|
|
222
|
+
|
|
223
|
+
# unpack ridge endpoints and vertex indices
|
|
224
|
+
p1 = rp[:, 0].copy()
|
|
225
|
+
p2 = rp[:, 1].copy()
|
|
226
|
+
v1 = rv[:, 0].copy()
|
|
227
|
+
v2 = rv[:, 1].copy()
|
|
228
|
+
|
|
229
|
+
valid_pts = (p1 >= 0) & (p2 >= 0)
|
|
230
|
+
|
|
231
|
+
# coordinates
|
|
232
|
+
P1 = np.zeros((num_ridges, 2), dtype=float); P2 = np.zeros((num_ridges, 2), dtype=float)
|
|
233
|
+
P1[valid_pts] = self.pts[p1[valid_pts]]
|
|
234
|
+
P2[valid_pts] = self.pts[p2[valid_pts]]
|
|
235
|
+
V1 = vertices_all[v1]
|
|
236
|
+
V2 = vertices_all[v2]
|
|
237
|
+
|
|
238
|
+
# enforce V1->V2 clockwise around p1 (swap only p1<->p2, not v1/v2)
|
|
239
|
+
P12 = P2 - P1
|
|
240
|
+
V12 = V2 - V1
|
|
241
|
+
swap = (P12[:, 0] * V12[:, 1] - P12[:, 1] * V12[:, 0]) > 0
|
|
242
|
+
p1_sw = p1.copy(); p2_sw = p2.copy()
|
|
243
|
+
p1_sw[swap] = p2[swap]; p2_sw[swap] = p1[swap]
|
|
244
|
+
p1, p2 = p1_sw, p2_sw
|
|
245
|
+
P1_sw = P1.copy(); P2_sw = P2.copy()
|
|
246
|
+
P1[swap] = P2_sw[swap]; P2[swap] = P1_sw[swap]
|
|
247
|
+
|
|
248
|
+
# "inner" tests relative to p1 (keep your exact choices)
|
|
249
|
+
r = self.phys.r
|
|
250
|
+
d1 = np.linalg.norm(V1 - P1, axis=1)
|
|
251
|
+
d2 = np.linalg.norm(V2 - P1, axis=1)
|
|
252
|
+
inner1 = (d1 <= r) & valid_pts
|
|
253
|
+
inner2 = (d2 <= r) & valid_pts
|
|
254
|
+
|
|
255
|
+
# segment/intersection
|
|
256
|
+
dV = V2 - V1
|
|
257
|
+
segL = np.linalg.norm(dV, axis=1)
|
|
258
|
+
denom = np.where(segL > 0.0, segL, 1.0)
|
|
259
|
+
dx, dy = dV[:, 0], dV[:, 1]
|
|
260
|
+
x, y = P1[:, 0], P1[:, 1]
|
|
261
|
+
x1, y1 = V1[:, 0], V1[:, 1]
|
|
262
|
+
t = ((x - x1) * dx + (y - y1) * dy) / denom
|
|
263
|
+
|
|
264
|
+
# mid-point C = (p1+p2)/2 (your exact choice)
|
|
265
|
+
C = 0.5 * (P1 + P2)
|
|
266
|
+
cx, cy = C[:, 0], C[:, 1]
|
|
267
|
+
t1 = -t
|
|
268
|
+
t2 = t1 + denom
|
|
269
|
+
|
|
270
|
+
d = np.linalg.norm(C - P1, axis=1)
|
|
271
|
+
has_int = (d < r) & valid_pts
|
|
272
|
+
tr = np.full_like(d, np.nan)
|
|
273
|
+
tr[has_int] = np.sqrt(r * r - d[has_int] * d[has_int])
|
|
274
|
+
|
|
275
|
+
cond1 = (-tr < t2) & (-tr > t1) & valid_pts
|
|
276
|
+
cond2 = ( tr < t2) & ( tr > t1) & valid_pts
|
|
277
|
+
|
|
278
|
+
invL = np.where(denom > 0.0, 1.0 / denom, 0.0)
|
|
279
|
+
xr1 = cx - tr * dx * invL
|
|
280
|
+
yr1 = cy - tr * dy * invL
|
|
281
|
+
xr2 = cx + tr * dx * invL
|
|
282
|
+
yr2 = cy + tr * dy * invL
|
|
283
|
+
|
|
284
|
+
# fill outer-vertex arrays only where intersections happen
|
|
285
|
+
idx1 = np.where(cond1)[0]
|
|
286
|
+
if idx1.size:
|
|
287
|
+
vertices_out[2 * idx1 + 0, 0] = xr1[idx1]
|
|
288
|
+
vertices_out[2 * idx1 + 0, 1] = yr1[idx1]
|
|
289
|
+
pairs1 = np.sort(np.stack([p1[idx1], p2[idx1]], axis=1), axis=1)
|
|
290
|
+
vertex_out_points[2 * idx1 + 0] = pairs1
|
|
291
|
+
|
|
292
|
+
idx2 = np.where(cond2)[0]
|
|
293
|
+
if idx2.size:
|
|
294
|
+
vertices_out[2 * idx2 + 1, 0] = xr2[idx2]
|
|
295
|
+
vertices_out[2 * idx2 + 1, 1] = yr2[idx2]
|
|
296
|
+
pairs2 = np.sort(np.stack([p1[idx2], p2[idx2]], axis=1), axis=1)
|
|
297
|
+
vertex_out_points[2 * idx2 + 1] = pairs2
|
|
298
|
+
|
|
299
|
+
# sets (same semantics as before)
|
|
300
|
+
vertex_out_id = set((num_vertices_ext + np.where((np.arange(2 * num_ridges) % 2 == 0) & cond1.repeat(2))[0]).tolist())
|
|
301
|
+
vertex_out_id.update((num_vertices_ext + np.where((np.arange(2 * num_ridges) % 2 == 1) & cond2.repeat(2))[0]).tolist())
|
|
302
|
+
|
|
303
|
+
vertex_in_id = set(v1[inner1].astype(int).tolist())
|
|
304
|
+
vertex_in_id.update(v2[inner2].astype(int).tolist())
|
|
305
|
+
|
|
306
|
+
# -------- NEW: packed arrays instead of ridge_info dict --------
|
|
307
|
+
# each endpoint has up to 3 entries; absent slots are -1
|
|
308
|
+
p1_edges_pack = np.full((num_ridges, 3), -1, dtype=int)
|
|
309
|
+
p1_verts_pack = np.full((num_ridges, 3), -1, dtype=int)
|
|
310
|
+
p2_edges_pack = np.full((num_ridges, 3), -1, dtype=int)
|
|
311
|
+
p2_verts_pack = np.full((num_ridges, 3), -1, dtype=int)
|
|
312
|
+
|
|
313
|
+
out_id1 = num_vertices_ext + (2 * np.arange(num_ridges) + 0)
|
|
314
|
+
out_id2 = out_id1 + 1
|
|
315
|
+
|
|
316
|
+
# p1 order: [inner1 -> (1,v1)], [cond1 -> (1,out1)], [cond2 -> (0,out2)]
|
|
317
|
+
p1_edges_pack[inner1, 0] = 1
|
|
318
|
+
p1_verts_pack[inner1, 0] = v1[inner1]
|
|
319
|
+
|
|
320
|
+
p1_edges_pack[cond1, 1] = 1
|
|
321
|
+
p1_verts_pack[cond1, 1] = out_id1[cond1]
|
|
322
|
+
|
|
323
|
+
p1_edges_pack[cond2, 2] = 0
|
|
324
|
+
p1_verts_pack[cond2, 2] = out_id2[cond2]
|
|
325
|
+
|
|
326
|
+
# p2 was "append then reverse", which yields final order: [inner2, cond2, cond1]
|
|
327
|
+
p2_edges_pack[inner2, 0] = 1
|
|
328
|
+
p2_verts_pack[inner2, 0] = v2[inner2]
|
|
329
|
+
|
|
330
|
+
p2_edges_pack[cond2, 1] = 1
|
|
331
|
+
p2_verts_pack[cond2, 1] = out_id2[cond2]
|
|
332
|
+
|
|
333
|
+
p2_edges_pack[cond1, 2] = 0
|
|
334
|
+
p2_verts_pack[cond1, 2] = out_id1[cond1]
|
|
335
|
+
|
|
336
|
+
# append outer-vertex slots (unused rows stay zero like before)
|
|
337
|
+
vertices_all = np.vstack([vertices_all, vertices_out])
|
|
338
|
+
|
|
339
|
+
# --------------------------------------------------
|
|
340
|
+
# Part 1 in Cython
|
|
341
|
+
# --------------------------------------------------
|
|
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(
|
|
344
|
+
vor_regions, vor.point_region.astype(np.int64),
|
|
345
|
+
vertices_all.astype(np.float64), pts.astype(np.float64),
|
|
346
|
+
int(num_vertices), vertexpair2ridge,
|
|
347
|
+
p1.astype(np.int64), p1_edges_pack.astype(np.int64), p1_verts_pack.astype(np.int64),
|
|
348
|
+
p2.astype(np.int64), p2_edges_pack.astype(np.int64), p2_verts_pack.astype(np.int64),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# --------------------------------------------------
|
|
352
|
+
# Part 2 in Cython
|
|
353
|
+
# --------------------------------------------------
|
|
354
|
+
vertex_out_da_dtheta, vertex_out_dl_dtheta, dA_poly_dh, dP_poly_dh, area_list, perimeter_list = compute_vertex_derivatives_cy(
|
|
355
|
+
point_edges_type, # list-of-lists / arrays of edge types
|
|
356
|
+
point_vertices_f_idx, # list-of-lists / arrays of vertex ids
|
|
357
|
+
vertices_all.astype(np.float64, copy=False),
|
|
358
|
+
pts.astype(np.float64, copy=False),
|
|
359
|
+
float(r),
|
|
360
|
+
float(A0),
|
|
361
|
+
float(P0),
|
|
362
|
+
int(num_vertices_ext),
|
|
363
|
+
int(num_ridges),
|
|
364
|
+
vertex_out_points.astype(np.int64, copy=False)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
diagnostics = dict(
|
|
368
|
+
vertex_in_id=set(vertex_in_id),
|
|
369
|
+
vertex_out_id=set(vertex_out_id),
|
|
370
|
+
vertices_out=vertices_out,
|
|
371
|
+
vertex_out_points=vertex_out_points,
|
|
372
|
+
vertex_out_da_dtheta=vertex_out_da_dtheta,
|
|
373
|
+
vertex_out_dl_dtheta=vertex_out_dl_dtheta,
|
|
374
|
+
dA_poly_dh=dA_poly_dh,
|
|
375
|
+
dP_poly_dh=dP_poly_dh,
|
|
376
|
+
area_list=area_list,
|
|
377
|
+
perimeter_list=perimeter_list,
|
|
378
|
+
point_edges_type=point_edges_type,
|
|
379
|
+
point_vertices_f_idx=point_vertices_f_idx,
|
|
380
|
+
num_vertices_ext=num_vertices_ext,
|
|
381
|
+
)
|
|
382
|
+
return diagnostics, vertices_all
|
|
383
|
+
|
|
384
|
+
# --------------------- Force assembly ---------------------
|
|
385
|
+
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,
|
|
388
|
+
vertex_out_dl_dtheta: np.ndarray, dA_poly_dh: np.ndarray, dP_poly_dh: np.ndarray,
|
|
389
|
+
area_list: np.ndarray, perimeter_list: np.ndarray) -> np.ndarray:
|
|
390
|
+
"""
|
|
391
|
+
Assemble forces on cell centers from polygon and arc contributions.
|
|
392
|
+
"""
|
|
393
|
+
N = self.N
|
|
394
|
+
r = self.phys.r
|
|
395
|
+
A0 = self.phys.A0
|
|
396
|
+
P0 = self.phys.P0
|
|
397
|
+
KA = self.phys.KA
|
|
398
|
+
KP = self.phys.KP
|
|
399
|
+
Lambda = self.phys.lambda_tension
|
|
400
|
+
pts = self.pts
|
|
401
|
+
|
|
402
|
+
dE_poly_dh = 2.0 * (KA * dA_poly_dh + KP * dP_poly_dh)
|
|
403
|
+
|
|
404
|
+
fx = np.zeros(N)
|
|
405
|
+
fy = np.zeros(N)
|
|
406
|
+
|
|
407
|
+
# ===============================================================
|
|
408
|
+
# (1) Inner vertices contributions — vectorized + bincount scatter
|
|
409
|
+
# ===============================================================
|
|
410
|
+
if len(vertex_in_id) > 0:
|
|
411
|
+
H = np.asarray(list(vertex_in_id), dtype=int) # (H,)
|
|
412
|
+
# unpack triples (i,j,k) for each inner vertex
|
|
413
|
+
I = np.empty(len(H), dtype=int)
|
|
414
|
+
J = np.empty(len(H), dtype=int)
|
|
415
|
+
K = np.empty(len(H), dtype=int)
|
|
416
|
+
for t, h in enumerate(H):
|
|
417
|
+
I[t], J[t], K[t] = vertex_points[h]
|
|
418
|
+
|
|
419
|
+
ri = pts[I] # (H,2)
|
|
420
|
+
rj = pts[J]
|
|
421
|
+
rk = pts[K]
|
|
422
|
+
|
|
423
|
+
rj_minus_rk = rj - rk
|
|
424
|
+
ri_minus_rj = ri - rj
|
|
425
|
+
ri_minus_rk = ri - rk
|
|
426
|
+
rj_minus_ri = -ri_minus_rj
|
|
427
|
+
rk_minus_ri = -ri_minus_rk
|
|
428
|
+
rk_minus_rj = rk - rj
|
|
429
|
+
|
|
430
|
+
D0 = _row_dot(ri - rj, np.column_stack((rj_minus_rk[:,1], -rj_minus_rk[:,0]))) # cross2(ri-rj, rj-rk)
|
|
431
|
+
# rewrite cross robustly:
|
|
432
|
+
D0 = (ri[:,0]-rj[:,0])*(rj_minus_rk[:,1]) - (ri[:,1]-rj[:,1])*(rj_minus_rk[:,0])
|
|
433
|
+
D = 2.0 * (D0 ** 2)
|
|
434
|
+
|
|
435
|
+
# alphas
|
|
436
|
+
alpha_i = _row_dot(rj_minus_rk, rj_minus_rk) * _row_dot(ri_minus_rj, ri_minus_rk) / D
|
|
437
|
+
alpha_j = _row_dot(ri_minus_rk, ri_minus_rk) * _row_dot(rj_minus_ri, rj_minus_rk) / D
|
|
438
|
+
alpha_k = _row_dot(ri_minus_rj, ri_minus_rj) * _row_dot(rk_minus_ri, rk_minus_rj) / D
|
|
439
|
+
|
|
440
|
+
# d_alpha_j / d_ri and d_alpha_k / d_ri
|
|
441
|
+
cross_z = np.column_stack((rj_minus_rk[:, 1], -rj_minus_rk[:, 0])) # (H,2)
|
|
442
|
+
term_j_ri = (rk_minus_rj / _row_dot(rj_minus_ri, rj_minus_rk)[:, None]) + 2.0 * (ri_minus_rk / _row_dot(ri_minus_rk, ri_minus_rk)[:, None]) - 2.0 * (cross_z / D0[:, None])
|
|
443
|
+
term_k_ri = (rj_minus_rk / _row_dot(rk_minus_ri, rk_minus_rj)[:, None]) + 2.0 * (ri_minus_rj / _row_dot(ri_minus_rj, ri_minus_rj)[:, None]) - 2.0 * (cross_z / D0[:, None])
|
|
444
|
+
d_alpha_j_d_ri = (alpha_j[:, None] * term_j_ri) # (H,2)
|
|
445
|
+
d_alpha_k_d_ri = (alpha_k[:, None] * term_k_ri)
|
|
446
|
+
|
|
447
|
+
d_h_in_d_xi = alpha_i[:, None] * np.array([1.0, 0.0]) + d_alpha_j_d_ri[:, [0]] * (rj - ri) + d_alpha_k_d_ri[:, [0]] * (rk - ri)
|
|
448
|
+
d_h_in_d_yi = alpha_i[:, None] * np.array([0.0, 1.0]) + d_alpha_j_d_ri[:, [1]] * (rj - ri) + d_alpha_k_d_ri[:, [1]] * (rk - ri)
|
|
449
|
+
|
|
450
|
+
# d_alpha_i / d_rj and d_alpha_k / d_rj
|
|
451
|
+
cross_z = np.column_stack((-(ri_minus_rk)[:, 1], (ri_minus_rk)[:, 0]))
|
|
452
|
+
term_i_rj = (rk_minus_ri / _row_dot(ri_minus_rj, ri_minus_rk)[:, None]) + 2.0 * (rj_minus_rk / _row_dot(rj_minus_rk, rj_minus_rk)[:, None]) - 2.0 * (cross_z / D0[:, None])
|
|
453
|
+
term_k_rj = (ri_minus_rk / _row_dot(rk_minus_rj, rk_minus_ri)[:, None]) + 2.0 * (rj_minus_ri / _row_dot(rj_minus_ri, rj_minus_ri)[:, None]) - 2.0 * (cross_z / D0[:, None])
|
|
454
|
+
d_alpha_i_d_rj = (alpha_i[:, None] * term_i_rj)
|
|
455
|
+
d_alpha_k_d_rj = (alpha_k[:, None] * term_k_rj)
|
|
456
|
+
|
|
457
|
+
d_h_in_d_xj = d_alpha_i_d_rj[:, [0]] * (ri - rj) + alpha_j[:, None] * np.array([1.0, 0.0]) + d_alpha_k_d_rj[:, [0]] * (rk - rj)
|
|
458
|
+
d_h_in_d_yj = d_alpha_i_d_rj[:, [1]] * (ri - rj) + alpha_j[:, None] * np.array([0.0, 1.0]) + d_alpha_k_d_rj[:, [1]] * (rk - rj)
|
|
459
|
+
|
|
460
|
+
# d_alpha_i / d_rk and d_alpha_j / d_rk
|
|
461
|
+
cross_z = np.column_stack(((ri_minus_rj)[:, 1], -(ri_minus_rj)[:, 0]))
|
|
462
|
+
term_i_rk = (rj_minus_ri / _row_dot(ri_minus_rk, ri_minus_rj)[:, None]) + 2.0 * (rk_minus_rj / _row_dot(rk_minus_rj, rk_minus_rj)[:, None]) - 2.0 * (cross_z / D0[:, None])
|
|
463
|
+
term_j_rk = (ri_minus_rj / _row_dot(rj_minus_rk, rj_minus_ri)[:, None]) + 2.0 * (rk_minus_ri / _row_dot(rk_minus_ri, rk_minus_ri)[:, None]) - 2.0 * (cross_z / D0[:, None])
|
|
464
|
+
d_alpha_i_d_rk = (alpha_i[:, None] * term_i_rk)
|
|
465
|
+
d_alpha_j_d_rk = (alpha_j[:, None] * term_j_rk)
|
|
466
|
+
|
|
467
|
+
d_h_in_d_xk = d_alpha_i_d_rk[:, [0]] * (ri - rk) + d_alpha_j_d_rk[:, [0]] * (rj - rk) + alpha_k[:, None] * np.array([1.0, 0.0])
|
|
468
|
+
d_h_in_d_yk = d_alpha_i_d_rk[:, [1]] * (ri - rk) + d_alpha_j_d_rk[:, [1]] * (rj - rk) + alpha_k[:, None] * np.array([0.0, 1.0])
|
|
469
|
+
|
|
470
|
+
deh = dE_poly_dh[H] # (H,2)
|
|
471
|
+
contrib_x_i = _row_dot(deh, d_h_in_d_xi)
|
|
472
|
+
contrib_x_j = _row_dot(deh, d_h_in_d_xj)
|
|
473
|
+
contrib_x_k = _row_dot(deh, d_h_in_d_xk)
|
|
474
|
+
contrib_y_i = _row_dot(deh, d_h_in_d_yi)
|
|
475
|
+
contrib_y_j = _row_dot(deh, d_h_in_d_yj)
|
|
476
|
+
contrib_y_k = _row_dot(deh, d_h_in_d_yk)
|
|
477
|
+
|
|
478
|
+
# bincount-based scatter (faster than repeated np.add.at)
|
|
479
|
+
fx += (
|
|
480
|
+
np.bincount(I, weights=contrib_x_i, minlength=N)
|
|
481
|
+
+ np.bincount(J, weights=contrib_x_j, minlength=N)
|
|
482
|
+
+ np.bincount(K, weights=contrib_x_k, minlength=N)
|
|
483
|
+
)
|
|
484
|
+
fy += (
|
|
485
|
+
np.bincount(I, weights=contrib_y_i, minlength=N)
|
|
486
|
+
+ np.bincount(J, weights=contrib_y_j, minlength=N)
|
|
487
|
+
+ np.bincount(K, weights=contrib_y_k, minlength=N)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# ===============================================================
|
|
491
|
+
# (2) Outer vertices contributions — vectorized; bincount scatter
|
|
492
|
+
# ===============================================================
|
|
493
|
+
dA_arc_dr = np.zeros((N, 2))
|
|
494
|
+
dP_arc_dr = np.zeros((N,2))
|
|
495
|
+
dL_dr = np.zeros((N,2))
|
|
496
|
+
|
|
497
|
+
if len(vertex_out_id) > 0:
|
|
498
|
+
Vsel = np.asarray(vertex_out_id, dtype=int) # absolute vertex IDs in vertices_all
|
|
499
|
+
h_idx = Vsel - num_vertices_ext # rows into vertex_out_* arrays
|
|
500
|
+
# guard against any accidental negatives / out-of-range
|
|
501
|
+
valid_mask = (h_idx >= 0) & (h_idx < len(vertex_out_points))
|
|
502
|
+
if np.any(valid_mask):
|
|
503
|
+
Vsel = Vsel[valid_mask]
|
|
504
|
+
h_idx = h_idx[valid_mask]
|
|
505
|
+
|
|
506
|
+
# geometry slices
|
|
507
|
+
h_out = vertices_all[Vsel] # (M,2)
|
|
508
|
+
IJ = np.asarray(vertex_out_points, dtype=int)[h_idx] # (M,2)
|
|
509
|
+
I = IJ[:, 0]
|
|
510
|
+
J = IJ[:, 1]
|
|
511
|
+
|
|
512
|
+
ri = pts[I] # (M,2)
|
|
513
|
+
rj = pts[J] # (M,2)
|
|
514
|
+
rij_vec = ri - rj
|
|
515
|
+
rij = np.linalg.norm(rij_vec, axis=1) # (M,)
|
|
516
|
+
root = np.sqrt(4.0 * (r ** 2) - (rij ** 2))
|
|
517
|
+
|
|
518
|
+
# sign based on orientation: sign(cross(h_out-rj, ri-rj))
|
|
519
|
+
sign = np.sign((h_out[:, 0] - rj[:, 0]) * (ri[:, 1] - rj[:, 1])
|
|
520
|
+
- (h_out[:, 1] - rj[:, 1]) * (ri[:, 0] - rj[:, 0]))
|
|
521
|
+
|
|
522
|
+
x_unit = np.array([1.0, 0.0])[None, :]
|
|
523
|
+
y_unit = np.array([0.0, 1.0])[None, :]
|
|
524
|
+
|
|
525
|
+
cross_z = rij_vec[:, [1]] * x_unit - rij_vec[:, [0]] * y_unit # (M,2)
|
|
526
|
+
denom = (np.maximum(root[:, None], self.phys.delta) * (rij ** 3)[:, None]) # small offset to avoid singularities
|
|
527
|
+
|
|
528
|
+
dx_terms = - (2.0 * (r ** 2) * rij_vec[:, [0]] * cross_z / denom) \
|
|
529
|
+
- (root / (2.0 * rij))[:, None] * y_unit
|
|
530
|
+
dy_terms = - (2.0 * (r ** 2) * rij_vec[:, [1]] * cross_z / denom) \
|
|
531
|
+
+ (root / (2.0 * rij))[:, None] * x_unit
|
|
532
|
+
|
|
533
|
+
d_h_out_d_xi = (x_unit / 2.0) + sign[:, None] * dx_terms
|
|
534
|
+
d_h_out_d_yi = (y_unit / 2.0) + sign[:, None] * dy_terms
|
|
535
|
+
d_h_out_d_xj = (x_unit / 2.0) - sign[:, None] * dx_terms
|
|
536
|
+
d_h_out_d_yj = (y_unit / 2.0) - sign[:, None] * dy_terms
|
|
537
|
+
|
|
538
|
+
# polygon part on these selected outer vertices
|
|
539
|
+
deh_out = dE_poly_dh[Vsel] # (M,2)
|
|
540
|
+
|
|
541
|
+
fx += (
|
|
542
|
+
np.bincount(I, weights=_row_dot(deh_out, d_h_out_d_xi), minlength=N)
|
|
543
|
+
+ np.bincount(J, weights=_row_dot(deh_out, d_h_out_d_xj), minlength=N)
|
|
544
|
+
)
|
|
545
|
+
fy += (
|
|
546
|
+
np.bincount(I, weights=_row_dot(deh_out, d_h_out_d_yi), minlength=N)
|
|
547
|
+
+ np.bincount(J, weights=_row_dot(deh_out, d_h_out_d_yj), minlength=N)
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# ---- arc angle sensitivities (per-cell accumulators) ----
|
|
551
|
+
u_i = h_out - ri
|
|
552
|
+
u_j = h_out - rj
|
|
553
|
+
inv_ui2 = 1.0 / _row_dot(u_i, u_i)
|
|
554
|
+
inv_uj2 = 1.0 / _row_dot(u_j, u_j)
|
|
555
|
+
u_perp_i = np.column_stack((-u_i[:, 1], u_i[:, 0])) * inv_ui2[:, None]
|
|
556
|
+
u_perp_j = np.column_stack((-u_j[:, 1], u_j[:, 0])) * inv_uj2[:, None]
|
|
557
|
+
|
|
558
|
+
d_theta_i_d_xj = _row_dot(d_h_out_d_xj, u_perp_i)
|
|
559
|
+
d_theta_i_d_yj = _row_dot(d_h_out_d_yj, u_perp_i)
|
|
560
|
+
d_theta_i_d_xi = -d_theta_i_d_xj
|
|
561
|
+
d_theta_i_d_yi = -d_theta_i_d_yj
|
|
562
|
+
|
|
563
|
+
d_theta_j_d_xi = _row_dot(d_h_out_d_xi, u_perp_j)
|
|
564
|
+
d_theta_j_d_yi = _row_dot(d_h_out_d_yi, u_perp_j)
|
|
565
|
+
d_theta_j_d_xj = -d_theta_j_d_xi
|
|
566
|
+
d_theta_j_d_yj = -d_theta_j_d_yi
|
|
567
|
+
|
|
568
|
+
# weights (only for the selected outer vertices h_idx)
|
|
569
|
+
v_da = vertex_out_da_dtheta[h_idx] # (M,2)
|
|
570
|
+
v_dl = vertex_out_dl_dtheta[h_idx] # (M,2)
|
|
571
|
+
|
|
572
|
+
Ai_w_i = (area_list[I] - A0) * v_da[:, 0]
|
|
573
|
+
Aj_w_j = (area_list[J] - A0) * v_da[:, 1]
|
|
574
|
+
Pi_w_i = (perimeter_list[I] - P0) * v_dl[:, 0]
|
|
575
|
+
Pj_w_j = (perimeter_list[J] - P0) * v_dl[:, 1]
|
|
576
|
+
|
|
577
|
+
# accumulate with bincount
|
|
578
|
+
dA_arc_dr[:, 0] += np.bincount(I, Ai_w_i * d_theta_i_d_xi + Aj_w_j * d_theta_j_d_xi, minlength=N)
|
|
579
|
+
dA_arc_dr[:, 0] += np.bincount(J, Ai_w_i * d_theta_i_d_xj + Aj_w_j * d_theta_j_d_xj, minlength=N)
|
|
580
|
+
dA_arc_dr[:, 1] += np.bincount(I, Ai_w_i * d_theta_i_d_yi + Aj_w_j * d_theta_j_d_yi, minlength=N)
|
|
581
|
+
dA_arc_dr[:, 1] += np.bincount(J, Ai_w_i * d_theta_i_d_yj + Aj_w_j * d_theta_j_d_yj, minlength=N)
|
|
582
|
+
|
|
583
|
+
dP_arc_dr[:, 0] += np.bincount(I, Pi_w_i * d_theta_i_d_xi + Pj_w_j * d_theta_j_d_xi, minlength=N)
|
|
584
|
+
dP_arc_dr[:, 0] += np.bincount(J, Pi_w_i * d_theta_i_d_xj + Pj_w_j * d_theta_j_d_xj, minlength=N)
|
|
585
|
+
dP_arc_dr[:, 1] += np.bincount(I, Pi_w_i * d_theta_i_d_yi + Pj_w_j * d_theta_j_d_yi, minlength=N)
|
|
586
|
+
dP_arc_dr[:, 1] += np.bincount(J, Pi_w_i * d_theta_i_d_yj + Pj_w_j * d_theta_j_d_yj, minlength=N)
|
|
587
|
+
|
|
588
|
+
# line-tension contributions for Lambda
|
|
589
|
+
dL_arc_x = v_dl[:, 0] * d_theta_i_d_xi + v_dl[:, 1] * d_theta_j_d_xi
|
|
590
|
+
dL_arc_y = v_dl[:, 0] * d_theta_i_d_yi + v_dl[:, 1] * d_theta_j_d_yi
|
|
591
|
+
dL_arc_xJ = v_dl[:, 0] * d_theta_i_d_xj + v_dl[:, 1] * d_theta_j_d_xj
|
|
592
|
+
dL_arc_yJ = v_dl[:, 0] * d_theta_i_d_yj + v_dl[:, 1] * d_theta_j_d_yj
|
|
593
|
+
|
|
594
|
+
dL_dr[:, 0] += np.bincount(I, dL_arc_x, minlength=N)
|
|
595
|
+
dL_dr[:, 1] += np.bincount(I, dL_arc_y, minlength=N)
|
|
596
|
+
dL_dr[:, 0] += np.bincount(J, dL_arc_xJ, minlength=N)
|
|
597
|
+
dL_dr[:, 1] += np.bincount(J, dL_arc_yJ, minlength=N)
|
|
598
|
+
|
|
599
|
+
# combine arc terms
|
|
600
|
+
dE_arc_dr = 2.0 * (KA * dA_arc_dr + KP * dP_arc_dr) + Lambda * dL_dr
|
|
601
|
+
|
|
602
|
+
fx = -(fx + dE_arc_dr[:, 0])
|
|
603
|
+
fy = -(fy + dE_arc_dr[:, 1])
|
|
604
|
+
|
|
605
|
+
F = np.zeros((N, 2), dtype=float)
|
|
606
|
+
F[:, 0] = fx
|
|
607
|
+
F[:, 1] = fy
|
|
608
|
+
return F
|
|
609
|
+
|
|
610
|
+
# --------------------- One integration step ---------------------
|
|
611
|
+
def build(self) -> Dict:
|
|
612
|
+
"""
|
|
613
|
+
Do the following:
|
|
614
|
+
- Build Voronoi (+ extensions)
|
|
615
|
+
- Get cell connectivity
|
|
616
|
+
- Compute per-cell quantities and derivatives
|
|
617
|
+
- Assemble forces
|
|
618
|
+
Returns a dictionary of diagnostics.
|
|
619
|
+
"""
|
|
620
|
+
(vor, vertices_all, ridge_vertices_all, num_vertices,
|
|
621
|
+
vertexpair2ridge, vertex_points) = self._build_voronoi_with_extensions()
|
|
622
|
+
|
|
623
|
+
connections = self._get_connections(vor.ridge_points, vertices_all, ridge_vertices_all)
|
|
624
|
+
|
|
625
|
+
geom, vertices_all = self._per_cell_geometry(vor, vertices_all, ridge_vertices_all, num_vertices, vertexpair2ridge)
|
|
626
|
+
|
|
627
|
+
F = self._assemble_forces(
|
|
628
|
+
vertices_all=vertices_all,
|
|
629
|
+
num_vertices_ext=geom["num_vertices_ext"],
|
|
630
|
+
vertex_points=vertex_points,
|
|
631
|
+
vertex_in_id=list(geom["vertex_in_id"]),
|
|
632
|
+
vertex_out_id=list(geom["vertex_out_id"]),
|
|
633
|
+
vertex_out_points=geom["vertex_out_points"],
|
|
634
|
+
vertex_out_da_dtheta=geom["vertex_out_da_dtheta"],
|
|
635
|
+
vertex_out_dl_dtheta=geom["vertex_out_dl_dtheta"],
|
|
636
|
+
dA_poly_dh=geom["dA_poly_dh"],
|
|
637
|
+
dP_poly_dh=geom["dP_poly_dh"],
|
|
638
|
+
area_list=geom["area_list"],
|
|
639
|
+
perimeter_list=geom["perimeter_list"],
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
return dict(
|
|
643
|
+
forces=F,
|
|
644
|
+
areas=geom["area_list"],
|
|
645
|
+
perimeters=geom["perimeter_list"],
|
|
646
|
+
vertices=vertices_all,
|
|
647
|
+
edges_type=geom["point_edges_type"],
|
|
648
|
+
regions=geom["point_vertices_f_idx"],
|
|
649
|
+
connections=connections,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# --------------------- 2D plotting utilities ---------------------
|
|
653
|
+
def plot_2d(self, ax: Optional[Axes] = None, show: bool = False) -> Axes:
|
|
654
|
+
"""
|
|
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
|
|
667
|
+
"""
|
|
668
|
+
(vor, vertices_all, ridge_vertices_all, num_vertices,
|
|
669
|
+
vertexpair2ridge, vertex_points) = self._build_voronoi_with_extensions()
|
|
670
|
+
|
|
671
|
+
geom, vertices_all = self._per_cell_geometry(vor, vertices_all, ridge_vertices_all, num_vertices, vertexpair2ridge)
|
|
672
|
+
|
|
673
|
+
if ax is None:
|
|
674
|
+
ax = plt.gca()
|
|
675
|
+
|
|
676
|
+
self._plot_routine(ax, vor, vertices_all, ridge_vertices_all,
|
|
677
|
+
geom["point_edges_type"], geom["point_vertices_f_idx"])
|
|
678
|
+
|
|
679
|
+
if show:
|
|
680
|
+
plt.show()
|
|
681
|
+
return ax
|
|
682
|
+
|
|
683
|
+
# --------------------- 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:
|
|
686
|
+
"""
|
|
687
|
+
Low-level plot routine. Draws:
|
|
688
|
+
- All Voronoi edges (solid for finite, dashed for formerly-infinite)
|
|
689
|
+
- Cell centers
|
|
690
|
+
- Each cell boundary (poly edges and circular arcs)
|
|
691
|
+
"""
|
|
692
|
+
pts = self.pts
|
|
693
|
+
r = self.phys.r
|
|
694
|
+
N = self.N
|
|
695
|
+
|
|
696
|
+
center = np.mean(pts, axis=0)
|
|
697
|
+
if N > 1:
|
|
698
|
+
span_x = np.ptp(pts[:, 0]) # pts span in x
|
|
699
|
+
span_y = np.ptp(pts[:, 1]) # pts span in y
|
|
700
|
+
L = max(span_x, span_y) + 3.0 * r
|
|
701
|
+
L *= 0.8
|
|
702
|
+
else:
|
|
703
|
+
L = 5.0 * r
|
|
704
|
+
|
|
705
|
+
# Draw Voronoi ridge segments
|
|
706
|
+
for idx in range(len(vor.ridge_vertices)):
|
|
707
|
+
x1, y1 = vertices_all[ridge_vertices_all[idx][0]]
|
|
708
|
+
x2, y2 = vertices_all[ridge_vertices_all[idx][1]]
|
|
709
|
+
if -1 not in vor.ridge_vertices[idx]:
|
|
710
|
+
ax.plot([x1, x2], [y1, y2], 'k-', lw=0.5)
|
|
711
|
+
else:
|
|
712
|
+
ax.plot([x1, x2], [y1, y2], 'k--', lw=0.5)
|
|
713
|
+
|
|
714
|
+
# Draw cell centers
|
|
715
|
+
ax.plot(pts[:, 0], pts[:, 1], 'o', color='C0', markersize=2)
|
|
716
|
+
|
|
717
|
+
# Draw each cell boundary
|
|
718
|
+
for idx in range(N):
|
|
719
|
+
edges_type = point_edges_type[idx]
|
|
720
|
+
vertices_f_idx = point_vertices_f_idx[idx]
|
|
721
|
+
|
|
722
|
+
x, y = pts[idx]
|
|
723
|
+
if len(edges_type) < 2:
|
|
724
|
+
angle = np.linspace(0, 2*np.pi, 100)
|
|
725
|
+
ax.plot(x + r * np.cos(angle), y + r * np.sin(angle), color="C6", zorder=2)
|
|
726
|
+
continue
|
|
727
|
+
|
|
728
|
+
for idx_f, edge_type in enumerate(edges_type):
|
|
729
|
+
v1_idx = vertices_f_idx[idx_f]
|
|
730
|
+
x1, y1 = vertices_all[v1_idx]
|
|
731
|
+
idx2 = idx_f + 1 if idx_f < len(edges_type)-1 else 0
|
|
732
|
+
v2_idx = vertices_f_idx[idx2]
|
|
733
|
+
x2, y2 = vertices_all[v2_idx]
|
|
734
|
+
|
|
735
|
+
if edge_type == 1:
|
|
736
|
+
ax.plot([x1, x2], [y1, y2], 'b-', zorder=1)
|
|
737
|
+
else:
|
|
738
|
+
angle1 = np.arctan2(y1-y, x1-x)
|
|
739
|
+
angle2 = np.arctan2(y2-y, x2-x)
|
|
740
|
+
dangle = np.linspace(0, (angle1 - angle2) % (2*np.pi), 100)
|
|
741
|
+
|
|
742
|
+
ax.plot(x + r * np.cos(angle2+dangle), y + r * np.sin(angle2+dangle), color="C6", zorder=2)
|
|
743
|
+
|
|
744
|
+
ax.set_aspect("equal")
|
|
745
|
+
ax.set_xlim(center[0]-L, center[0]+L)
|
|
746
|
+
ax.set_ylim(center[1]-L, center[1]+L)
|
|
747
|
+
|
|
748
|
+
# --------------------- 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:
|
|
750
|
+
"""
|
|
751
|
+
Determine which pairs of cells are connected, i.e.,
|
|
752
|
+
the distance from the cell center to its corresponding Voronoi ridge
|
|
753
|
+
segment is < self.phys.r.
|
|
754
|
+
"""
|
|
755
|
+
ridge_points_arr = np.asarray(ridge_points, dtype=int).reshape(-1, 2) # (R, 2)
|
|
756
|
+
ridge_vertices_arr = np.asarray(ridge_vertices_all, dtype=int).reshape(-1, 2) # (R, 2)
|
|
757
|
+
|
|
758
|
+
# take p2 for each ridge, avoid -1 points (representing space)
|
|
759
|
+
p1_idx = ridge_points_arr[:, 0] # (R,)
|
|
760
|
+
p2_idx = ridge_points_arr[:, 1] # (R,)
|
|
761
|
+
p2 = self.pts[p2_idx] # (R, 2)
|
|
762
|
+
|
|
763
|
+
v1 = vertices_all[ridge_vertices_arr[:, 0]] # (R, 2)
|
|
764
|
+
v2 = vertices_all[ridge_vertices_arr[:, 1]] # (R, 2)
|
|
765
|
+
|
|
766
|
+
# vectorized point-to-segment distance
|
|
767
|
+
AB = v2 - v1 # (R, 2)
|
|
768
|
+
AP = p2 - v1 # (R, 2)
|
|
769
|
+
denom = np.einsum("ij,ij->i", AB, AB) # (R,)
|
|
770
|
+
|
|
771
|
+
t = np.einsum("ij,ij->i", AP, AB) / denom
|
|
772
|
+
t = np.clip(t, 0.0, 1.0)[:, None] # (R,1)
|
|
773
|
+
|
|
774
|
+
C = v1 + t * AB # closest point on segment, (R,2)
|
|
775
|
+
dists = np.linalg.norm(p2 - C, axis=1) # (R,)
|
|
776
|
+
|
|
777
|
+
mask = dists < self.phys.r
|
|
778
|
+
|
|
779
|
+
connect = np.stack([p1_idx[mask], p2_idx[mask]], axis=1)
|
|
780
|
+
if connect.size > 0:
|
|
781
|
+
connect = np.sort(connect, axis=1)
|
|
782
|
+
else:
|
|
783
|
+
connect = np.empty((0, 2), dtype=int)
|
|
784
|
+
return connect
|
|
785
|
+
|
|
786
|
+
# --------------------- Update positions ---------------------
|
|
787
|
+
def update_positions(self, pts: np.ndarray) -> None:
|
|
788
|
+
"""
|
|
789
|
+
Update cell center positions.
|
|
790
|
+
"""
|
|
791
|
+
self.N, dim = pts.shape
|
|
792
|
+
if dim != 2:
|
|
793
|
+
raise ValueError("Positions must have shape (N,2)")
|
|
794
|
+
|
|
795
|
+
self.pts = pts
|
|
796
|
+
|
|
797
|
+
# --------------------- Update physical parameters ---------------------
|
|
798
|
+
def update_params(self, phys: PhysicalParams) -> None:
|
|
799
|
+
"""
|
|
800
|
+
Update physical parameters.
|
|
801
|
+
"""
|
|
802
|
+
self.phys = phys
|