pyafv 0.3.1__cp313-cp313-musllinux_1_2_i686.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 ADDED
@@ -0,0 +1,11 @@
1
+ from .physical_params import PhysicalParams, target_delta
2
+ from .simulator import FiniteVoronoiSimulator
3
+
4
+
5
+ __version__ = "0.3.1"
6
+
7
+ __all__ = [
8
+ "PhysicalParams",
9
+ "FiniteVoronoiSimulator",
10
+ "target_delta",
11
+ ]
pyafv/backend.py ADDED
@@ -0,0 +1,16 @@
1
+ # chooses fast vs fallback implementation
2
+
3
+ try:
4
+ from . import finite_voronoi_fast as _impl
5
+ _BACKEND_NAME = "cython"
6
+ except ImportError: # pragma: no cover
7
+ _BACKEND_NAME = "python"
8
+ from . import finite_voronoi_fallback as _impl
9
+
10
+ # ---- for explicit API ----
11
+ backend_simulator = _impl.FiniteVoronoiSimulator
12
+
13
+ __all__ = [
14
+ "backend_simulator",
15
+ "_BACKEND_NAME",
16
+ ]
pyafv/cell_geom.pyx ADDED
@@ -0,0 +1,508 @@
1
+ # cython: boundscheck=False, wraparound=False, nonecheck=False, cdivision=True, initializedcheck=False
2
+ # distutils: language = c++
3
+
4
+ import numpy as np
5
+ cimport numpy as cnp
6
+ from libc.math cimport atan2, cos
7
+ cimport cython
8
+
9
+ # Routine used in _build_voronoi_with_extensions()
10
+ def build_vertexpair_and_vertexpoints_cy(object ridge_vertices_all,
11
+ object ridge_points,
12
+ long num_vertices,
13
+ long N):
14
+ # --- accept any int dtype and make sure it's contiguous int64 ---
15
+ cdef cnp.ndarray[cnp.int64_t, ndim=2] rv = np.ascontiguousarray(ridge_vertices_all, dtype=np.int64)
16
+ cdef cnp.ndarray[cnp.int64_t, ndim=2] rp = np.ascontiguousarray(ridge_points, dtype=np.int64)
17
+ # ----------------------------------------------------------------
18
+
19
+ cdef Py_ssize_t R = rv.shape[0]
20
+ cdef Py_ssize_t k, v1, v2, v_id, ridge_id
21
+ cdef Py_ssize_t i_pt, j_pt
22
+
23
+ cdef dict vertex_incident_ridges = {}
24
+ cdef dict vertexpair2ridge = {}
25
+ cdef dict vertex_points = {}
26
+ cdef list ridges_for_v
27
+ cdef set s
28
+
29
+ for k in range(R):
30
+ v1 = <Py_ssize_t> rv[k, 0]
31
+ v2 = <Py_ssize_t> rv[k, 1]
32
+
33
+ if v1 not in vertex_incident_ridges:
34
+ vertex_incident_ridges[v1] = []
35
+ if v2 not in vertex_incident_ridges:
36
+ vertex_incident_ridges[v2] = []
37
+
38
+ (<list>vertex_incident_ridges[v1]).append(k)
39
+ (<list>vertex_incident_ridges[v2]).append(k)
40
+
41
+ vertexpair2ridge[(<int>v1, <int>v2)] = <int>k
42
+ vertexpair2ridge[(<int>v2, <int>v1)] = <int>k
43
+
44
+ if N > 2:
45
+ for v_id in range(num_vertices):
46
+ if v_id not in vertex_incident_ridges:
47
+ continue
48
+ ridges_for_v = <list>vertex_incident_ridges[v_id]
49
+ s = set()
50
+ for ridge_id in ridges_for_v:
51
+ i_pt = <int> rp[ridge_id, 0]
52
+ j_pt = <int> rp[ridge_id, 1]
53
+ s.add(i_pt); s.add(j_pt)
54
+ vertex_points[v_id] = list(s)
55
+
56
+ return vertexpair2ridge, vertex_points
57
+
58
+
59
+ # ---------------------------------------------------------------------------------------
60
+ # Part 1
61
+ # ---------------------------------------------------------------------------------------
62
+
63
+ def pad_regions_cy(object regions):
64
+ cdef Py_ssize_t n = len(regions)
65
+ cdef Py_ssize_t i, j, m, kmax = 0
66
+ cdef list r
67
+
68
+ for i in range(n):
69
+ m = len(<list>regions[i])
70
+ if m > kmax:
71
+ kmax = m
72
+
73
+ cdef cnp.ndarray[cnp.int64_t, ndim=2] out = \
74
+ np.empty((n, kmax), dtype=np.int64)
75
+
76
+ # fill with -1
77
+ for i in range(n):
78
+ for j in range(kmax):
79
+ out[i, j] = -1
80
+
81
+ # copy rows
82
+ for i in range(n):
83
+ r = <list>regions[i]
84
+ m = len(r)
85
+ for j in range(m):
86
+ out[i, j] = <long long>r[j]
87
+
88
+ return out
89
+
90
+ @cython.cfunc
91
+ cdef inline long long pack_pair_ll(long a, long b) nogil:
92
+ return ((<long long>a) << 32) | (<unsigned long long>b)
93
+
94
+ @cython.boundscheck(False)
95
+ @cython.wraparound(False)
96
+ def build_point_edges_cy(
97
+ cnp.int64_t[:, :] vor_regions,
98
+ cnp.int64_t[:] point_region,
99
+ cnp.float64_t[:, :] vertices_all,
100
+ cnp.float64_t[:, :] pts,
101
+ long num_vertices,
102
+ dict vertexpair2ridge_py,
103
+ cnp.int64_t[:] p1,
104
+ cnp.int64_t[:, :] p1_edges_pack,
105
+ cnp.int64_t[:, :] p1_verts_pack,
106
+ cnp.int64_t[:] p2,
107
+ cnp.int64_t[:, :] p2_edges_pack,
108
+ cnp.int64_t[:, :] p2_verts_pack
109
+ ):
110
+ cdef Py_ssize_t N = pts.shape[0]
111
+ cdef list point_edges_type = [None] * N
112
+ cdef list point_vertices_f_idx = [None] * N
113
+
114
+ # packed (v1,v2) -> ridge_id
115
+ cdef dict pair2ridge = {}
116
+ cdef object k, rid_val_py
117
+ for k, rid_val_py in vertexpair2ridge_py.items():
118
+ pair2ridge[ ((<long long>(<tuple>k)[0])<<32) | (<unsigned long long>(<tuple>k)[1]) ] = <int>rid_val_py
119
+
120
+ # declarations
121
+ cdef Py_ssize_t idx, j, m, ecount, n_valid
122
+ cdef int rid
123
+ cdef long region_id, v1, v2
124
+ cdef cnp.int64_t[:] region_row
125
+ cdef cnp.float64_t cx, cy, vx, vy
126
+ cdef int Rmax = vor_regions.shape[1]
127
+ cdef bint use_p1_flag
128
+ cdef long val
129
+ cdef list edges_type
130
+ cdef list vertices_f_idx
131
+
132
+ # scratch arrays as Python objects + memoryviews
133
+ cdef object angles_np = np.empty(Rmax, dtype=np.float64)
134
+ cdef cnp.float64_t[:] angles = angles_np
135
+ cdef object valid_np = np.empty(Rmax, dtype=np.uint8)
136
+ cdef cnp.uint8_t[:] valid = valid_np
137
+
138
+ cdef object v_ids_np
139
+ cdef object v2_ids_np
140
+ cdef cnp.int64_t[:] v_ids
141
+ cdef cnp.int64_t[:] v2_ids
142
+
143
+ cdef cnp.int64_t[:, :] pack_e
144
+ cdef cnp.int64_t[:, :] pack_v
145
+
146
+ for idx in range(N):
147
+ region_id = <long>point_region[idx]
148
+ if region_id < 0:
149
+ point_edges_type[idx] = []
150
+ point_vertices_f_idx[idx] = []
151
+ continue
152
+
153
+ # 1) collect region vertex ids (stop at -1)
154
+ region_row = vor_regions[region_id]
155
+ m = 0
156
+ while m < Rmax and region_row[m] != -1:
157
+ m += 1
158
+ if m == 0:
159
+ point_edges_type[idx] = []
160
+ point_vertices_f_idx[idx] = []
161
+ continue
162
+
163
+ v_ids_np = np.empty(m, dtype=np.int64)
164
+ v_ids = v_ids_np
165
+ for j in range(m):
166
+ v_ids[j] = region_row[j]
167
+
168
+ # 2) angles around cell center
169
+ cx = pts[idx, 0]; cy = pts[idx, 1]
170
+ for j in range(m):
171
+ vx = vertices_all[v_ids[j], 0] - cx
172
+ vy = vertices_all[v_ids[j], 1] - cy
173
+ angles[j] = atan2(vy, vx)
174
+
175
+ # 3) sort clockwise (descending angle)
176
+ order = np.argsort(np.asarray(angles_np)[:m]) # Python object
177
+ v_ids_np = np.asarray(v_ids_np)[order[::-1]] # new array
178
+ v_ids = v_ids_np # rebind memoryview to the new array
179
+
180
+ # 4) consecutive pairs with wrap
181
+ v2_ids_np = np.empty(m, dtype=np.int64)
182
+ v2_ids = v2_ids_np
183
+ for j in range(m-1):
184
+ v2_ids[j] = v_ids[j+1]
185
+ v2_ids[m-1] = v_ids[0]
186
+
187
+ # 5) mask: skip ray-ray
188
+ n_valid = 0
189
+ for j in range(m):
190
+ valid[j] = not ((v_ids[j] >= num_vertices) and (v2_ids[j] >= num_vertices))
191
+ if valid[j]:
192
+ n_valid += 1
193
+ if n_valid == 0:
194
+ point_edges_type[idx] = []
195
+ point_vertices_f_idx[idx] = []
196
+ continue
197
+
198
+ # 6) gather packs
199
+ pack_e = np.empty((n_valid, 3), dtype=np.int64)
200
+ pack_v = np.empty((n_valid, 3), dtype=np.int64)
201
+ ecount = 0
202
+
203
+ for j in range(m):
204
+ if not valid[j]:
205
+ continue
206
+ v1 = <long>v_ids[j]
207
+ v2 = <long>v2_ids[j]
208
+
209
+ rid = <int>pair2ridge.get(((<long long>v1)<<32) | (<unsigned long long>v2), -1)
210
+ if rid < 0:
211
+ rid = <int>vertexpair2ridge_py.get((v1, v2), -1)
212
+ if rid < 0:
213
+ raise KeyError(f"Missing ridge id for pair ({v1}, {v2})")
214
+
215
+ use_p1_flag = (p1[rid] == idx)
216
+ if use_p1_flag:
217
+ pack_e[ecount, 0] = p1_edges_pack[rid, 0]
218
+ pack_e[ecount, 1] = p1_edges_pack[rid, 1]
219
+ pack_e[ecount, 2] = p1_edges_pack[rid, 2]
220
+ pack_v[ecount, 0] = p1_verts_pack[rid, 0]
221
+ pack_v[ecount, 1] = p1_verts_pack[rid, 1]
222
+ pack_v[ecount, 2] = p1_verts_pack[rid, 2]
223
+ else:
224
+ pack_e[ecount, 0] = p2_edges_pack[rid, 0]
225
+ pack_e[ecount, 1] = p2_edges_pack[rid, 1]
226
+ pack_e[ecount, 2] = p2_edges_pack[rid, 2]
227
+ pack_v[ecount, 0] = p2_verts_pack[rid, 0]
228
+ pack_v[ecount, 1] = p2_verts_pack[rid, 1]
229
+ pack_v[ecount, 2] = p2_verts_pack[rid, 2]
230
+ ecount += 1
231
+
232
+ # 7) flatten valid entries
233
+ edges_type = []
234
+ vertices_f_idx = []
235
+ for j in range(n_valid):
236
+ val = pack_e[j, 0]
237
+ if val >= 0:
238
+ edges_type.append(<int>val)
239
+ vertices_f_idx.append(<int>pack_v[j, 0])
240
+ val = pack_e[j, 1]
241
+ if val >= 0:
242
+ edges_type.append(<int>val)
243
+ vertices_f_idx.append(<int>pack_v[j, 1])
244
+ val = pack_e[j, 2]
245
+ if val >= 0:
246
+ edges_type.append(<int>val)
247
+ vertices_f_idx.append(<int>pack_v[j, 2])
248
+
249
+ if len(edges_type) != len(vertices_f_idx):
250
+ raise ValueError("Vertex and edge number not equal!")
251
+
252
+ point_edges_type[idx] = edges_type
253
+ point_vertices_f_idx[idx] = vertices_f_idx
254
+
255
+ return point_edges_type, point_vertices_f_idx
256
+
257
+
258
+ # ---------------------------------------------------------------------------------------
259
+ # Part 2
260
+ # ---------------------------------------------------------------------------------------
261
+
262
+ @cython.cfunc
263
+ cdef inline double cross2(double ax, double ay, double bx, double by) nogil:
264
+ return ax*by - ay*bx
265
+
266
+ @cython.cfunc
267
+ cdef inline void perp(double ux, double uy, double* outx, double* outy) noexcept nogil:
268
+ outx[0] = uy
269
+ outy[0] = -ux
270
+
271
+ @cython.boundscheck(False)
272
+ @cython.wraparound(False)
273
+ def compute_vertex_derivatives_cy(
274
+ object point_edges_type, # list of 1D int arrays/lists
275
+ object point_vertices_f_idx, # list of 1D int arrays/lists
276
+ cnp.float64_t[:, :] vertices_all, # (num_vertices_ext + 2*num_ridges, 2)
277
+ cnp.float64_t[:, :] pts, # (N, 2)
278
+ double r,
279
+ double A0,
280
+ double P0,
281
+ long num_vertices_ext,
282
+ long num_ridges,
283
+ cnp.int64_t[:, :] vertex_out_points # (2*num_ridges, 2), rows sorted [i,j], col1=max(i,j)
284
+ ):
285
+ cdef Py_ssize_t N = pts.shape[0]
286
+ cdef Py_ssize_t i, j, E
287
+ cdef double two_pi = 6.2831853071795864769
288
+
289
+ # outputs
290
+ cdef object vertex_out_da_dtheta_np = np.zeros((2*num_ridges, 2), dtype=np.float64)
291
+ cdef object vertex_out_dl_dtheta_np = np.zeros((2*num_ridges, 2), dtype=np.float64)
292
+ cdef object dA_poly_dh_np = np.zeros((num_vertices_ext + 2*num_ridges, 2), dtype=np.float64)
293
+ cdef object dP_poly_dh_np = np.zeros((num_vertices_ext + 2*num_ridges, 2), dtype=np.float64)
294
+ cdef object area_list_np = np.zeros(N, dtype=np.float64)
295
+ cdef object perimeter_list_np = np.zeros(N, dtype=np.float64)
296
+
297
+ cdef cnp.float64_t[:, :] vertex_out_da_dtheta = vertex_out_da_dtheta_np
298
+ cdef cnp.float64_t[:, :] vertex_out_dl_dtheta = vertex_out_dl_dtheta_np
299
+ cdef cnp.float64_t[:, :] dA_poly_dh = dA_poly_dh_np
300
+ cdef cnp.float64_t[:, :] dP_poly_dh = dP_poly_dh_np
301
+ cdef cnp.float64_t[:] area_list = area_list_np
302
+ cdef cnp.float64_t[:] perimeter_list = perimeter_list_np
303
+
304
+ # declarations used inside loop (declare here, assign in loop)
305
+ cdef object edges_type_obj
306
+ cdef object v_idx_obj
307
+ cdef cnp.int64_t[:] edges_type
308
+ cdef cnp.int64_t[:] vertices_f_idx
309
+
310
+ # scalars
311
+ cdef double Pi_straight, Ai_straight, Pi_arc, Ai_arc, Pi, Ai
312
+ cdef double cx, cy
313
+ cdef double v1x, v1y, v2x, v2y, v0x, v0y
314
+ cdef double v1mx, v1my, v2mx, v2my, v0mx, v0my
315
+ cdef double s12x, s12y, s10x, s10y
316
+ cdef double l12, l10
317
+ cdef double a1, a2, dangle, da1_full, da2_full
318
+ cdef double dAi_v1_x, dAi_v1_y
319
+ cdef double dPi_v1_x, dPi_v1_y
320
+ cdef long vidx, vprev
321
+ cdef long outer_row, which_col
322
+ cdef long k1, k2
323
+
324
+ # moved here (was causing your error inside the loop)
325
+ cdef double p2x, p2y, p0x, p0y
326
+
327
+ # small boolean arrays per cell
328
+ cdef object mask_str_np
329
+ cdef cnp.uint8_t[:] mask_str
330
+ cdef object mask_prev_str_np
331
+ cdef cnp.uint8_t[:] mask_prev_str
332
+ cdef object mask_arc_np
333
+ cdef cnp.uint8_t[:] mask_arc
334
+
335
+ # arrays per cell
336
+ cdef object v1_idx_np
337
+ cdef object v2_idx_np
338
+ cdef object v0_idx_np
339
+ cdef cnp.int64_t[:] v1_idx
340
+ cdef cnp.int64_t[:] v2_idx
341
+ cdef cnp.int64_t[:] v0_idx
342
+
343
+ cdef object a1_full_np
344
+ cdef object a2_full_np
345
+ cdef cnp.float64_t[:] a1_full
346
+ cdef cnp.float64_t[:] a2_full
347
+
348
+ cdef object dangle_full_np
349
+ cdef cnp.float64_t[:] dangle_full
350
+
351
+ for i in range(N):
352
+ edges_type_obj = point_edges_type[i]
353
+ v_idx_obj = point_vertices_f_idx[i]
354
+
355
+ # ensure ndarray[int64]
356
+ if not isinstance(edges_type_obj, np.ndarray) or (<cnp.ndarray>edges_type_obj).dtype.num != cnp.NPY_INT64:
357
+ edges_type_obj = np.asarray(edges_type_obj, dtype=np.int64)
358
+ if not isinstance(v_idx_obj, np.ndarray) or (<cnp.ndarray>v_idx_obj).dtype.num != cnp.NPY_INT64:
359
+ v_idx_obj = np.asarray(v_idx_obj, dtype=np.int64)
360
+
361
+ # assign to memoryviews (no cdef here)
362
+ edges_type = edges_type_obj
363
+ vertices_f_idx = v_idx_obj
364
+ E = vertices_f_idx.shape[0]
365
+
366
+ if E < 2:
367
+ area_list[i] = 3.14159265358979323846 * (r * r)
368
+ perimeter_list[i] = 2.0 * 3.14159265358979323846 * r
369
+ continue
370
+
371
+ # ring indices
372
+ v1_idx_np = np.empty(E, dtype=np.int64)
373
+ v2_idx_np = np.empty(E, dtype=np.int64)
374
+ v0_idx_np = np.empty(E, dtype=np.int64)
375
+ v1_idx = v1_idx_np; v2_idx = v2_idx_np; v0_idx = v0_idx_np
376
+
377
+ for j in range(E):
378
+ v1_idx[j] = vertices_f_idx[j]
379
+ v2_idx[j] = vertices_f_idx[(j+1) % E]
380
+ v0_idx[j] = vertices_f_idx[(j-1+E) % E]
381
+
382
+ cx = pts[i, 0]; cy = pts[i, 1]
383
+
384
+ # masks
385
+ mask_str_np = np.empty(E, dtype=np.uint8)
386
+ mask_arc_np = np.empty(E, dtype=np.uint8)
387
+ mask_prev_str_np = np.empty(E, dtype=np.uint8)
388
+ mask_str = mask_str_np; mask_arc = mask_arc_np; mask_prev_str = mask_prev_str_np
389
+
390
+ for j in range(E):
391
+ mask_str[j] = 1 if edges_type[j] == 1 else 0
392
+ mask_arc[j] = 0 if mask_str[j] else 1
393
+
394
+ # ----- perimeter & area -----
395
+ Pi_straight = 0.0
396
+ Ai_straight = 0.0
397
+
398
+ for j in range(E):
399
+ if mask_str[j]:
400
+ v1x = vertices_all[v1_idx[j], 0]; v1y = vertices_all[v1_idx[j], 1]
401
+ v2x = vertices_all[v2_idx[j], 0]; v2y = vertices_all[v2_idx[j], 1]
402
+ v1mx = v1x - cx; v1my = v1y - cy
403
+ v2mx = v2x - cx; v2my = v2y - cy
404
+ s12x = v1x - v2x; s12y = v1y - v2y
405
+ l12 = (s12x*s12x + s12y*s12y) ** 0.5
406
+ Pi_straight += l12
407
+ Ai_straight += -0.5 * cross2(v1mx, v1my, v2mx, v2my)
408
+
409
+ Pi_arc = 0.0
410
+ Ai_arc = 0.0
411
+
412
+ a1_full_np = np.empty(E, dtype=np.float64)
413
+ a2_full_np = np.empty(E, dtype=np.float64)
414
+ dangle_full_np = np.empty(E, dtype=np.float64)
415
+ a1_full = a1_full_np; a2_full = a2_full_np; dangle_full = dangle_full_np
416
+
417
+ for j in range(E):
418
+ v1x = vertices_all[v1_idx[j], 0]; v1y = vertices_all[v1_idx[j], 1]
419
+ v2x = vertices_all[v2_idx[j], 0]; v2y = vertices_all[v2_idx[j], 1]
420
+ v1mx = v1x - cx; v1my = v1y - cy
421
+ v2mx = v2x - cx; v2my = v2y - cy
422
+ a1 = atan2(v1my, v1mx)
423
+ a2 = atan2(v2my, v2mx)
424
+ dangle = a1 - a2
425
+ if dangle < 0.0:
426
+ dangle += two_pi
427
+ a1_full[j] = a1
428
+ a2_full[j] = a2
429
+ dangle_full[j] = dangle
430
+ if mask_arc[j]:
431
+ Pi_arc += r * dangle
432
+ Ai_arc += 0.5 * (r * r) * dangle
433
+
434
+ Pi = Pi_straight + Pi_arc
435
+ Ai = Ai_straight + Ai_arc
436
+ perimeter_list[i] = Pi
437
+ area_list[i] = Ai
438
+
439
+ # ----- dA_poly/dh, dP_poly/dh for v1 -----
440
+ for j in range(E):
441
+ # V2-R, V0-R
442
+ v2x = vertices_all[v2_idx[j], 0]; v2y = vertices_all[v2_idx[j], 1]
443
+ v0x = vertices_all[v0_idx[j], 0]; v0y = vertices_all[v0_idx[j], 1]
444
+ v2mx = v2x - cx; v2my = v2y - cy
445
+ v0mx = v0x - cx; v0my = v0y - cy
446
+
447
+ # use predeclared p2x,p2y,p0x,p0y
448
+ perp(v2mx, v2my, &p2x, &p2y)
449
+ perp(v0mx, v0my, &p0x, &p0y)
450
+ dAi_v1_x = -0.5 * p2x + 0.5 * p0x
451
+ dAi_v1_y = -0.5 * p2y + 0.5 * p0y
452
+
453
+ dPi_v1_x = 0.0; dPi_v1_y = 0.0
454
+
455
+ # current edge j: between v1 and v2
456
+ if mask_str[j]:
457
+ v1x = vertices_all[v1_idx[j], 0]; v1y = vertices_all[v1_idx[j], 1]
458
+ s12x = v1x - v2x; s12y = v1y - v2y
459
+ l12 = (s12x*s12x + s12y*s12y) ** 0.5
460
+ if l12 > 0.0:
461
+ dPi_v1_x += s12x / l12
462
+ dPi_v1_y += s12y / l12
463
+
464
+ # previous edge j-1: between v0 and v1
465
+ vprev = (j - 1 + E) % E
466
+ mask_prev_str[j] = mask_str[vprev]
467
+ if mask_prev_str[j]:
468
+ v0x = vertices_all[v0_idx[j], 0]; v0y = vertices_all[v0_idx[j], 1]
469
+ v1x = vertices_all[v1_idx[j], 0]; v1y = vertices_all[v1_idx[j], 1]
470
+ s10x = v1x - v0x; s10y = v1y - v0y
471
+ l10 = (s10x*s10x + s10y*s10y) ** 0.5
472
+ if l10 > 0.0:
473
+ dPi_v1_x += s10x / l10
474
+ dPi_v1_y += s10y / l10
475
+
476
+ vidx = v1_idx[j]
477
+ dA_poly_dh[vidx, 0] += (Ai - A0) * dAi_v1_x
478
+ dA_poly_dh[vidx, 1] += (Ai - A0) * dAi_v1_y
479
+ dP_poly_dh[vidx, 0] += (Pi - P0) * dPi_v1_x
480
+ dP_poly_dh[vidx, 1] += (Pi - P0) * dPi_v1_y
481
+
482
+ # ----- arc endpoint sensitivities -----
483
+ for j in range(E):
484
+ if mask_arc[j]:
485
+ # v1 endpoint
486
+ k1 = v1_idx[j] - num_vertices_ext
487
+ if k1 >= 0:
488
+ da1_full = 0.5 * (r*r) * (1.0 - cos(dangle_full[j]))
489
+ outer_row = k1
490
+ which_col = 1 if vertex_out_points[outer_row, 1] <= i else 0
491
+ vertex_out_da_dtheta[outer_row, which_col] = da1_full
492
+ vertex_out_dl_dtheta[outer_row, which_col] = r
493
+
494
+ # v2 endpoint
495
+ k2 = v2_idx[j] - num_vertices_ext
496
+ if k2 >= 0:
497
+ da2_full = -0.5 * (r*r) * (1.0 - cos(dangle_full[j]))
498
+ outer_row = k2
499
+ which_col = 1 if vertex_out_points[outer_row, 1] <= i else 0
500
+ vertex_out_da_dtheta[outer_row, which_col] = da2_full
501
+ vertex_out_dl_dtheta[outer_row, which_col] = -r
502
+
503
+ return (vertex_out_da_dtheta_np,
504
+ vertex_out_dl_dtheta_np,
505
+ dA_poly_dh_np,
506
+ dP_poly_dh_np,
507
+ area_list_np,
508
+ perimeter_list_np)