emerge 1.0.6__py3-none-any.whl → 1.1.0__py3-none-any.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.

Potentially problematic release.


This version of emerge might be problematic. Click here for more details.

@@ -0,0 +1,667 @@
1
+ # EMerge is an open source Python based FEM EM simulation module.
2
+ # Copyright (C) 2025 Robert Fennis.
3
+
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 2
7
+ # of the License, or (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see
16
+ # <https://www.gnu.org/licenses/>.
17
+
18
+ from .microwave_data import MWField
19
+ import numpy as np
20
+ from ...mth.optimized import matmul, outward_normal, cross_c
21
+ from numba import njit, f8, c16, i8, types, prange # type: ignore, p
22
+ from loguru import logger
23
+
24
+ def print_sparam_matrix(pre: str, S: np.ndarray):
25
+ """
26
+ Print an N x N complex S-parameter matrix in dB∠deg format.
27
+ Magnitude in dB rounded to 2 decimals, phase in degrees with 1 decimal.
28
+ """
29
+ S = np.asarray(S)
30
+ if S.ndim != 2 or S.shape[0] != S.shape[1]:
31
+ raise ValueError("S must be a square (N x N) complex matrix")
32
+
33
+ N = S.shape[0]
34
+ logger.debug(pre+"S-parameter matrix (dB ∠ deg):")
35
+
36
+ for i in range(N):
37
+ row_str = []
38
+ for j in range(N):
39
+ mag_db = 20 * np.log10(np.abs(S[i, j]) + np.finfo(float).eps)
40
+ phase_deg = np.degrees(np.angle(S[i, j]))
41
+ row_str.append(f"{mag_db:6.2f} dB ∠ {phase_deg:6.1f}°")
42
+ logger.debug(" | ".join(row_str))
43
+
44
+ def compute_convergence(Sold: np.ndarray, Snew: np.ndarray) -> float:
45
+ """
46
+ Return a single scalar: max |Snew - Sold|.
47
+ Works for shapes (N,N) or (..., N, N); reduces over all axes.
48
+ """
49
+
50
+ Sold = np.asarray(Sold)
51
+ Snew = np.asarray(Snew)
52
+ print_sparam_matrix('Old:',Sold)
53
+ print_sparam_matrix('New',Snew)
54
+ if Sold.shape != Snew.shape:
55
+ raise ValueError("Sold and Snew must have identical shapes")
56
+ #amp_conv = float(np.abs(np.abs(Snew) - np.abs(Sold)).max())
57
+ mag_conv = float(np.abs(np.abs(Snew)-np.abs(Sold)).max())
58
+ amp_conv = float(np.abs(Snew - Sold).max())
59
+ phase_conv = float(np.abs(np.angle(np.diag(Sold)/np.diag(Snew))).max()) * 180/np.pi
60
+ return amp_conv, mag_conv, phase_conv
61
+
62
+ def select_refinement_indices(errors: np.ndarray, refine: float) -> np.ndarray:
63
+ """
64
+ Pick indices to refine based on two rules, then take the smaller set:
65
+ (A) All indices with |error| >= (1 - refine) * max(|error|)
66
+ (B) The top ceil(refine * N) indices by |error|
67
+ Returns indices sorted from largest to smallest |error|.
68
+ """
69
+ errs = np.abs(np.ravel(errors))
70
+ N = errs.size
71
+ if N == 0:
72
+ return np.array([], dtype=int)
73
+ refine = float(np.clip(refine, 0.0, 1.0))
74
+ if refine == 0.0:
75
+ return np.array([], dtype=int)
76
+
77
+ # Sorted indices by decreasing amplitude
78
+ sorted_desc = np.argsort(errs)[::-1]
79
+
80
+ # Rule A: threshold by amplitude
81
+ thresh = (1.0 - refine) * errs[sorted_desc[0]]
82
+ A = np.flatnonzero(errs >= thresh)
83
+
84
+ # Rule B: top-k by count
85
+ k = int(np.ceil(refine * N))
86
+ B = sorted_desc[:k]
87
+
88
+ # Choose the smaller set (tie-breaker: use B)
89
+ chosen = B#A if A.size > B.size else B
90
+
91
+ # Return chosen indices sorted from largest to smallest amplitude
92
+ mask = np.zeros(N, dtype=bool)
93
+ mask[chosen] = True
94
+ return sorted_desc[mask[sorted_desc]]
95
+
96
+ @njit(f8[:](i8, f8[:,:], f8, f8, f8[:]), cache=True, nogil=True, parallel=False)
97
+ def compute_size(id: int, coords: np.ndarray, q: float, scaler: float, dss: np.ndarray) -> float:
98
+ """Optimized function to compute the size impressed by size constraint points on each other size constraint point.
99
+
100
+ Args:
101
+ id (int): _description_
102
+ coords (np.ndarray): _description_
103
+ q (float): _description_
104
+ scaler (float): _description_
105
+ dss (np.ndarray): _description_
106
+
107
+ Returns:
108
+ float: _description_
109
+ """
110
+ N = dss.shape[0]
111
+ sizes = np.zeros((N,), dtype=np.float64)-1.0
112
+ x, y, z = coords[:,id]
113
+ for n in prange(N):
114
+ if n == id:
115
+ sizes[n] = dss[n]*scaler
116
+ continue
117
+ nsize = scaler*dss[n]/q - (1-q)/q * ((x-coords[0,n])**2 + (y-coords[1,n])**2 + (z-coords[2,n])**2)**0.5
118
+ sizes[n] = nsize
119
+ return sizes
120
+
121
+ @njit(f8[:](f8[:,:], i8, i8[:]), cache=True, nogil=True, parallel=True)
122
+ def nbmin(matrix, axis, include):
123
+
124
+ if axis==0:
125
+ N = matrix.shape[1]
126
+ out = np.empty((N,), dtype=np.float64)
127
+ for n in prange(N):
128
+ out[n] = np.min(matrix[include==1,n])
129
+ return out
130
+ if axis==1:
131
+ N = matrix.shape[0]
132
+ out = np.empty((N,), dtype=np.float64)
133
+ for n in prange(N):
134
+ out[n] = np.min(matrix[n,include==1])
135
+ return out
136
+ else:
137
+ out = np.empty((N,), dtype=np.float64)
138
+ return out
139
+
140
+ @njit(i8(i8, f8[:,:], f8, i8[:]), cache=True, nogil=True, parallel=False)
141
+ def can_remove(index: int, M: np.ndarray, scaling: float, include: np.ndarray) -> int:
142
+
143
+ ratio = np.min(M[index,:] / nbmin(M, 1, include))
144
+
145
+ if ratio > scaling:
146
+ return 1
147
+ return 0
148
+
149
+ @njit(i8[:](f8[:,:], f8, f8[:], f8, f8), cache=True, nogil=True, parallel=False)
150
+ def reduce_point_set(coords: np.ndarray, q: float, dss: np.ndarray, scaler: float, keep_percentage: float) -> list[int]:
151
+ N = dss.shape[0]
152
+ impressed_size = np.zeros((N,N), np.float64)
153
+
154
+ include = np.ones((N,), dtype=np.int64)
155
+
156
+ for n in range(N):
157
+ impressed_size[:,n] = compute_size(n, coords, q, scaler, dss)
158
+
159
+ current_min = nbmin(impressed_size, 1, include)
160
+
161
+ counter = 0
162
+ for i in range(N):
163
+
164
+ if include[i]==0:
165
+ continue
166
+
167
+ if (N-counter)/N < keep_percentage:
168
+ break
169
+
170
+ n_imposed = np.sum(impressed_size[:,i] <= (current_min*1.01))
171
+
172
+ if n_imposed == 0:
173
+ include[i] = 0
174
+ counter = counter + 1
175
+
176
+
177
+
178
+ ids = np.arange(N)
179
+ output = ids[include==1]
180
+ return output
181
+
182
+
183
+ @njit(i8[:, :](i8[:], i8[:, :]), cache=True, nogil=True)
184
+ def local_mapping(vertex_ids, triangle_ids):
185
+ """
186
+ Parameters
187
+ ----------
188
+ vertex_ids : 1-D int64 array (length 4)
189
+ Global vertex 0.1005964238ers of one tetrahedron, in *its* order
190
+ (v0, v1, v2, v3).
191
+
192
+ triangle_ids : 2-D int64 array (nTri × 3)
193
+ Each row is a global-ID triple of one face that belongs to this tet.
194
+
195
+ Returns
196
+ -------
197
+ local_tris : 2-D int64 array (nTri × 3)
198
+ Same triangles, but every entry replaced by the local index
199
+ 0,1,2,3 that the vertex has inside this tetrahedron.
200
+ (Guaranteed to be ∈{0,1,2,3}; no -1 ever appears if the input
201
+ really belongs to the tet.)
202
+ """
203
+ ndim = triangle_ids.shape[0]
204
+ ntri = triangle_ids.shape[1]
205
+ out = np.zeros(triangle_ids.shape, dtype=np.int64)
206
+
207
+ for t in range(ntri): # each triangle
208
+ for j in range(ndim): # each vertex in that triangle
209
+ gid = triangle_ids[j, t] # global ID to look up
210
+
211
+ # linear search over the four tet vertices
212
+ for k in range(4):
213
+ if vertex_ids[k] == gid:
214
+ out[j, t] = k # store local index 0-3
215
+ break # stop the k-loop
216
+
217
+ return out
218
+
219
+ @njit(f8(f8[:], f8[:], f8[:]), cache = True, nogil=True)
220
+ def compute_volume(xs, ys, zs):
221
+ x1, x2, x3, x4 = xs
222
+ y1, y2, y3, y4 = ys
223
+ z1, z2, z3, z4 = zs
224
+
225
+ return np.abs(-x1*y2*z3/6 + x1*y2*z4/6 + x1*y3*z2/6 - x1*y3*z4/6 - x1*y4*z2/6 + x1*y4*z3/6 + x2*y1*z3/6 - x2*y1*z4/6 - x2*y3*z1/6 + x2*y3*z4/6 + x2*y4*z1/6 - x2*y4*z3/6 - x3*y1*z2/6 + x3*y1*z4/6 + x3*y2*z1/6 - x3*y2*z4/6 - x3*y4*z1/6 + x3*y4*z2/6 + x4*y1*z2/6 - x4*y1*z3/6 - x4*y2*z1/6 + x4*y2*z3/6 + x4*y3*z1/6 - x4*y3*z2/6)
226
+
227
+ @njit(types.Tuple((f8[:], f8[:], f8[:], f8[:], f8))(f8[:], f8[:], f8[:]), cache = True, nogil=True)
228
+ def tet_coefficients(xs, ys, zs):
229
+ ## THIS FUNCTION WORKS
230
+ x1, x2, x3, x4 = xs
231
+ y1, y2, y3, y4 = ys
232
+ z1, z2, z3, z4 = zs
233
+
234
+ aas = np.empty((4,), dtype=np.float64)
235
+ bbs = np.empty((4,), dtype=np.float64)
236
+ ccs = np.empty((4,), dtype=np.float64)
237
+ dds = np.empty((4,), dtype=np.float64)
238
+
239
+ V = np.abs(-x1*y2*z3/6 + x1*y2*z4/6 + x1*y3*z2/6 - x1*y3*z4/6 - x1*y4*z2/6 + x1*y4*z3/6 + x2*y1*z3/6 - x2*y1*z4/6 - x2*y3*z1/6 + x2*y3*z4/6 + x2*y4*z1/6 - x2*y4*z3/6 - x3*y1*z2/6 + x3*y1*z4/6 + x3*y2*z1/6 - x3*y2*z4/6 - x3*y4*z1/6 + x3*y4*z2/6 + x4*y1*z2/6 - x4*y1*z3/6 - x4*y2*z1/6 + x4*y2*z3/6 + x4*y3*z1/6 - x4*y3*z2/6)
240
+
241
+ aas[0] = x2*y3*z4 - x2*y4*z3 - x3*y2*z4 + x3*y4*z2 + x4*y2*z3 - x4*y3*z2
242
+ aas[1] = -x1*y3*z4 + x1*y4*z3 + x3*y1*z4 - x3*y4*z1 - x4*y1*z3 + x4*y3*z1
243
+ aas[2] = x1*y2*z4 - x1*y4*z2 - x2*y1*z4 + x2*y4*z1 + x4*y1*z2 - x4*y2*z1
244
+ aas[3] = -x1*y2*z3 + x1*y3*z2 + x2*y1*z3 - x2*y3*z1 - x3*y1*z2 + x3*y2*z1
245
+ bbs[0] = -y2*z3 + y2*z4 + y3*z2 - y3*z4 - y4*z2 + y4*z3
246
+ bbs[1] = y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3
247
+ bbs[2] = -y1*z2 + y1*z4 + y2*z1 - y2*z4 - y4*z1 + y4*z2
248
+ bbs[3] = y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2
249
+ ccs[0] = x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3
250
+ ccs[1] = -x1*z3 + x1*z4 + x3*z1 - x3*z4 - x4*z1 + x4*z3
251
+ ccs[2] = x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2
252
+ ccs[3] = -x1*z2 + x1*z3 + x2*z1 - x2*z3 - x3*z1 + x3*z2
253
+ dds[0] = -x2*y3 + x2*y4 + x3*y2 - x3*y4 - x4*y2 + x4*y3
254
+ dds[1] = x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3
255
+ dds[2] = -x1*y2 + x1*y4 + x2*y1 - x2*y4 - x4*y1 + x4*y2
256
+ dds[3] = x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2
257
+
258
+ return aas, bbs, ccs, dds, V
259
+
260
+ @njit(c16[:](f8[:], f8[:,:], c16[:], i8[:,:], i8[:,:]), cache=True, nogil=True)
261
+ def compute_field(coords: np.ndarray,
262
+ vertices: np.ndarray,
263
+ Etet: np.ndarray,
264
+ l_edge_ids: np.ndarray,
265
+ l_tri_ids: np.ndarray):
266
+
267
+ x = coords[0]
268
+ y = coords[1]
269
+ z = coords[2]
270
+
271
+ xvs = vertices[0,:]
272
+ yvs = vertices[1,:]
273
+ zvs = vertices[2,:]
274
+
275
+ a_s, b_s, c_s, d_s, V = tet_coefficients(xvs, yvs, zvs)
276
+
277
+ Em1s = Etet[0:6]
278
+ Ef1s = Etet[6:10]
279
+ Em2s = Etet[10:16]
280
+ Ef2s = Etet[16:20]
281
+
282
+ Exl = 0.0 + 0.0j
283
+ Eyl = 0.0 + 0.0j
284
+ Ezl = 0.0 + 0.0j
285
+
286
+ V1 = (216*V**3)
287
+ for ie in range(6):
288
+ Em1, Em2 = Em1s[ie], Em2s[ie]
289
+ edgeids = l_edge_ids[:, ie]
290
+ a1, a2 = a_s[edgeids]
291
+ b1, b2 = b_s[edgeids]
292
+ c1, c2 = c_s[edgeids]
293
+ d1, d2 = d_s[edgeids]
294
+ x1, x2 = xvs[edgeids]
295
+ y1, y2 = yvs[edgeids]
296
+ z1, z2 = zvs[edgeids]
297
+ F1 = (a1 + b1*x + c1*y + d1*z)
298
+ F2 = (a2 + b2*x + c2*y + d2*z)
299
+ L = np.sqrt((x1 - x2)**2 + (y1 - y2)**2 + (z1 - z2)**2)
300
+ ex = L*(Em1*F1 + Em2*F2)*(b1*F2 - b2*F1)/V1
301
+ ey = L*(Em1*F1 + Em2*F2)*(c1*F2 - c2*F1)/V1
302
+ ez = L*(Em1*F1 + Em2*F2)*(d1*F2 - d2*F1)/V1
303
+
304
+ Exl += ex
305
+ Eyl += ey
306
+ Ezl += ez
307
+
308
+ for ie in range(4):
309
+ Em1, Em2 = Ef1s[ie], Ef2s[ie]
310
+ triids = l_tri_ids[:, ie]
311
+ a1, a2, a3 = a_s[triids]
312
+ b1, b2, b3 = b_s[triids]
313
+ c1, c2, c3 = c_s[triids]
314
+ d1, d2, d3 = d_s[triids]
315
+
316
+ x1, x2, x3 = xvs[l_tri_ids[:, ie]]
317
+ y1, y2, y3 = yvs[l_tri_ids[:, ie]]
318
+ z1, z2, z3 = zvs[l_tri_ids[:, ie]]
319
+
320
+ L1 = np.sqrt((x1-x3)**2 + (y1-y3)**2 + (z1-z3)**2)
321
+ L2 = np.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
322
+
323
+ F1 = (a1 + b1*x + c1*y + d1*z)
324
+ F2 = (a2 + b2*x + c2*y + d2*z)
325
+ F3 = (a3 + b3*x + c3*y + d3*z)
326
+
327
+ Q1 = Em1*L1*F2
328
+ Q2 = Em2*L2*F3
329
+ ex = (-Q1*(b1*F3 - b3*F1) + Q2*(b1*F2 - b2*F1))/V1
330
+ ey = (-Q1*(c1*F3 - c3*F1) + Q2*(c1*F2 - c2*F1))/V1
331
+ ez = (-Q1*(d1*F3 - d3*F1) + Q2*(d1*F2 - d2*F1))/V1
332
+
333
+ Exl += ex
334
+ Eyl += ey
335
+ Ezl += ez
336
+
337
+ out = np.zeros((3,), dtype=np.complex128)
338
+ out[0] = Exl
339
+ out[1] = Eyl
340
+ out[2] = Ezl
341
+ return out
342
+
343
+ @njit(c16[:,:](f8[:,:], f8[:,:], c16[:], i8[:,:], i8[:,:]), cache=True, nogil=True)
344
+ def compute_curl(coords: np.ndarray,
345
+ vertices: np.ndarray,
346
+ Etet: np.ndarray,
347
+ l_edge_ids: np.ndarray,
348
+ l_tri_ids: np.ndarray):
349
+
350
+ x = coords[0,:]
351
+ y = coords[1,:]
352
+ z = coords[2,:]
353
+
354
+ xvs = vertices[0,:]
355
+ yvs = vertices[1,:]
356
+ zvs = vertices[2,:]
357
+
358
+ a_s, b_s, c_s, d_s, V = tet_coefficients(xvs, yvs, zvs)
359
+
360
+ Em1s = Etet[0:6]
361
+ Ef1s = Etet[6:10]
362
+ Em2s = Etet[10:16]
363
+ Ef2s = Etet[16:20]
364
+
365
+ Exl = np.zeros((x.shape[0],), dtype=np.complex128)
366
+ Eyl = np.zeros((x.shape[0],), dtype=np.complex128)
367
+ Ezl = np.zeros((x.shape[0],), dtype=np.complex128)
368
+
369
+ V1 = (216*V**3)
370
+ V2 = (72*V**3)
371
+
372
+ for ie in range(6):
373
+ Em1, Em2 = Em1s[ie], Em2s[ie]
374
+ edgeids = l_edge_ids[:, ie]
375
+ a1, a2 = a_s[edgeids]
376
+ b1, b2 = b_s[edgeids]
377
+ c1, c2 = c_s[edgeids]
378
+ d1, d2 = d_s[edgeids]
379
+ x1, x2 = xvs[edgeids]
380
+ y1, y2 = yvs[edgeids]
381
+ z1, z2 = zvs[edgeids]
382
+
383
+ L = np.sqrt((x1 - x2)**2 + (y1 - y2)**2 + (z1 - z2)**2)
384
+ C1 = Em1*a1
385
+ C2 = Em1*b1
386
+ C3 = Em1*c1
387
+ C4 = Em1*c2
388
+ C5 = Em2*a2
389
+ C6 = Em2*b2
390
+ C7 = Em2*c1
391
+ C8 = Em2*c2
392
+ C9 = Em1*b2
393
+ C10 = Em2*b1
394
+ D1 = c1*d2
395
+ D2 = c2*d1
396
+ D3 = d1*d2
397
+ D4 = d1*d1
398
+ D5 = c2*d2
399
+ D6 = d2*d2
400
+ D7 = b1*d2
401
+ D8 = b2*d1
402
+ D9 = c1*d1
403
+ D10 = b2*d2
404
+ D11 = b1*c2
405
+ D12 = b2*c1
406
+ D13 = c1*c2
407
+ D14 = c1*c1
408
+ D15 = b2*c2
409
+
410
+ ex = L*(-C1*D1 + C1*D2 - C2*D1*x + C2*D2*x - C3*D1*y + C3*D2*y - C3*D3*z + C4*D4*z - C5*D1 + C5*D2 - C6*D1*x + C6*D2*x - C7*D5*y - C7*D6*z + C8*D2*y + C8*D3*z)/V2
411
+ ey = L*(C1*D7 - C1*D8 + C2*D7*x - C2*D8*x + C2*D1*y + C2*D3*z - C9*D9*y - C9*D4*z + C5*D7 - C5*D8 + C10*D10*x + C10*D5*y + C10*D6*z - C6*D8*x - C6*D2*y - C6*D3*z)/V2
412
+ ez = L*(-C1*D11 + C1*D12 - C2*D11*x + C2*D12*x - C2*D13*y - C2*D2*z + C9*D14*y + C9*D9*z - C5*D11 + C5*D12 - C10*D15*x - C10*c2*c2*y - C10*D5*z + C6*D12*x + C6*D13*y + C6*D1*z)/V2
413
+ Exl += ex
414
+ Eyl += ey
415
+ Ezl += ez
416
+
417
+ for ie in range(4):
418
+ Em1, Em2 = Ef1s[ie], Ef2s[ie]
419
+ triids = l_tri_ids[:, ie]
420
+ a1, a2, a3 = a_s[triids]
421
+ b1, b2, b3 = b_s[triids]
422
+ c1, c2, c3 = c_s[triids]
423
+ d1, d2, d3 = d_s[triids]
424
+
425
+ x1, x2, x3 = xvs[l_tri_ids[:, ie]]
426
+ y1, y2, y3 = yvs[l_tri_ids[:, ie]]
427
+ z1, z2, z3 = zvs[l_tri_ids[:, ie]]
428
+
429
+ L1 = np.sqrt((x1-x3)**2 + (y1-y3)**2 + (z1-z3)**2)
430
+ L2 = np.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
431
+ F1 = (a3 + b3*x + c3*y + d3*z)
432
+ F2 = (a1 + b1*x + c1*y + d1*z)
433
+ F3 = (a2 + b2*x + c2*y + d2*z)
434
+ N1 = (d1*F1 - d3*F2)
435
+ N2 = (c1*F1 - c3*F2)
436
+ N3 = (c1*d3 - c3*d1)
437
+ N4 = (d1*F3 - d2*F2)
438
+ N5 = (c1*F3 - c2*F2)
439
+ D1 = c1*d2
440
+ D2 = c2*d1
441
+ N6 = (D1 - D2)
442
+ N7 = (b1*F1 - b3*F2)
443
+ N8 = (b1*d3 - b3*d1)
444
+ N9 = (b1*F3 - b2*F2)
445
+ D7 = b1*d2
446
+ D8 = b2*d1
447
+ N10 = (D7 - D8)
448
+ D11 = b1*c2
449
+ D12 = b2*c1
450
+ ex = (Em1*L1*(-c2*N1 + d2*N2 + 2*N3*F3) - Em2*L2*(-c3*N4 + d3*N5 + 2*N6*F1))/V1
451
+ ey = (-Em1*L1*(-b2*N1 + d2*N7 + 2*N8*F3) + Em2*L2*(-b3*N4 + d3*N9 + 2*N10*F1))/V1
452
+ ez = (Em1*L1*(-b2*N2 + c2*N7 + 2*(b1*c3 - b3*c1)*F3) - Em2*L2*(-b3*N5 + c3*N9 + 2*(D11 - D12)*F1))/V1
453
+
454
+ Exl += ex
455
+ Eyl += ey
456
+ Ezl += ez
457
+
458
+ out = np.zeros((3,x.shape[0]), dtype=np.complex128)
459
+ out[0,:] = Exl
460
+ out[1,:] = Eyl
461
+ out[2,:] = Ezl
462
+ return out
463
+
464
+ @njit(c16[:](f8[:], f8[:,:], c16[:], i8[:,:], i8[:,:], c16[:,:]), cache=True, nogil=True)
465
+ def compute_curl_curl(coords: np.ndarray,
466
+ vertices: np.ndarray,
467
+ Etet: np.ndarray,
468
+ l_edge_ids: np.ndarray,
469
+ l_tri_ids: np.ndarray,
470
+ Um: np.ndarray):
471
+
472
+ uxx, uxy, uxz = Um[0,0], Um[0,1], Um[0,2]
473
+ uyx, uyy, uyz = Um[1,0], Um[1,1], Um[1,2]
474
+ uzx, uzy, uzz = Um[2,0], Um[2,1], Um[2,2]
475
+
476
+ x = coords[0]
477
+ y = coords[1]
478
+ z = coords[2]
479
+
480
+ xvs = vertices[0,:]
481
+ yvs = vertices[1,:]
482
+ zvs = vertices[2,:]
483
+
484
+ Exl = 0.0 + 0.0j
485
+ Eyl = 0.0 + 0.0j
486
+ Ezl = 0.0 + 0.0j
487
+
488
+ a_s, b_s, c_s, d_s, V = tet_coefficients(xvs, yvs, zvs)
489
+
490
+ Em1s = Etet[0:6]
491
+ Ef1s = Etet[6:10]
492
+ Em2s = Etet[10:16]
493
+ Ef2s = Etet[16:20]
494
+
495
+ V1 = (216*V**3)
496
+
497
+ for ie in range(6):
498
+ Em1, Em2 = Em1s[ie], Em2s[ie]
499
+ edgeids = l_edge_ids[:, ie]
500
+ a1, a2 = a_s[edgeids]
501
+ b1, b2 = b_s[edgeids]
502
+ c1, c2 = c_s[edgeids]
503
+ d1, d2 = d_s[edgeids]
504
+ x1, x2 = xvs[edgeids]
505
+ y1, y2 = yvs[edgeids]
506
+ z1, z2 = zvs[edgeids]
507
+
508
+ L1 = np.sqrt((x1 - x2)**2 + (y1 - y2)**2 + (z1 - z2)**2)
509
+ ex = -3*L1*(Em1*(b1*c1*c2*uzz - b1*c1*d2*uzy - b1*c2*d1*uyz + b1*d1*d2*uyy - b2*c1**2*uzz + b2*c1*d1*uyz + b2*c1*d1*uzy - b2*d1**2*uyy + c1**2*d2*uzx - c1*c2*d1*uzx - c1*d1*d2*uyx + c2*d1**2*uyx) + Em2*(b1*c2**2*uzz - b1*c2*d2*uyz - b1*c2*d2*uzy + b1*d2**2*uyy - b2*c1*c2*uzz + b2*c1*d2*uyz + b2*c2*d1*uzy - b2*d1*d2*uyy + c1*c2*d2*uzx - c1*d2**2*uyx - c2**2*d1*uzx + c2*d1*d2*uyx))
510
+ ey = 3*L1*(Em1*(b1**2*c2*uzz - b1**2*d2*uzy - b1*b2*c1*uzz + b1*b2*d1*uzy + b1*c1*d2*uzx - b1*c2*d1*uxz - b1*c2*d1*uzx + b1*d1*d2*uxy + b2*c1*d1*uxz - b2*d1**2*uxy - c1*d1*d2*uxx + c2*d1**2*uxx) + Em2*(b1*b2*c2*uzz - b1*b2*d2*uzy - b1*c2*d2*uxz + b1*d2**2*uxy - b2**2*c1*uzz + b2**2*d1*uzy + b2*c1*d2*uxz + b2*c1*d2*uzx - b2*c2*d1*uzx - b2*d1*d2*uxy - c1*d2**2*uxx + c2*d1*d2*uxx))
511
+ ez = -3*L1*(Em1*(b1**2*c2*uyz - b1**2*d2*uyy - b1*b2*c1*uyz + b1*b2*d1*uyy - b1*c1*c2*uxz + b1*c1*d2*uxy + b1*c1*d2*uyx - b1*c2*d1*uyx + b2*c1**2*uxz - b2*c1*d1*uxy - c1**2*d2*uxx + c1*c2*d1*uxx) + Em2*(b1*b2*c2*uyz - b1*b2*d2*uyy - b1*c2**2*uxz + b1*c2*d2*uxy - b2**2*c1*uyz + b2**2*d1*uyy + b2*c1*c2*uxz + b2*c1*d2*uyx - b2*c2*d1*uxy - b2*c2*d1*uyx - c1*c2*d2*uxx + c2**2*d1*uxx))
512
+
513
+ Exl += ex*V1
514
+ Eyl += ey*V1
515
+ Ezl += ez*V1
516
+
517
+ for ie in range(4):
518
+ Em1, Em2 = Ef1s[ie], Ef2s[ie]
519
+ triids = l_tri_ids[:, ie]
520
+ a1, a2, a3 = a_s[triids]
521
+ b1, b2, b3 = b_s[triids]
522
+ c1, c2, c3 = c_s[triids]
523
+ d1, d2, d3 = d_s[triids]
524
+
525
+ x1, x2, x3 = xvs[l_tri_ids[:, ie]]
526
+ y1, y2, y3 = yvs[l_tri_ids[:, ie]]
527
+ z1, z2, z3 = zvs[l_tri_ids[:, ie]]
528
+
529
+ L1 = np.sqrt((x1-x3)**2 + (y1-y3)**2 + (z1-z3)**2)
530
+ L2 = np.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
531
+
532
+ ex = L1*(Em1*L1*(3*c2*uzx*(c1*d3 - c3*d1) + 3*c2*uzz*(b1*c3 - b3*c1) - 3*d2*uyx*(c1*d3 - c3*d1) + 3*d2*uyy*(b1*d3 - b3*d1) - uyz*(-b2*(c1*d3 - c3*d1) + c2*(b1*d3 - b3*d1) + 2*d2*(b1*c3 - b3*c1)) - uzy*(b2*(c1*d3 - c3*d1) + 2*c2*(b1*d3 - b3*d1) + d2*(b1*c3 - b3*c1))) - Em2*L2*(3*c3*uzx*(c1*d2 - c2*d1) + 3*c3*uzz*(b1*c2 - b2*c1) - 3*d3*uyx*(c1*d2 - c2*d1) + 3*d3*uyy*(b1*d2 - b2*d1) - uyz*(-b3*(c1*d2 - c2*d1) + c3*(b1*d2 - b2*d1) + 2*d3*(b1*c2 - b2*c1)) - uzy*(b3*(c1*d2 - c2*d1) + 2*c3*(b1*d2 - b2*d1) + d3*(b1*c2 - b2*c1))))
533
+ ey = L1*(Em1*L1*(3*b2*uzy*(b1*d3 - b3*d1) - 3*b2*uzz*(b1*c3 - b3*c1) + 3*d2*uxx*(c1*d3 - c3*d1) - 3*d2*uxy*(b1*d3 - b3*d1) + uxz*(-b2*(c1*d3 - c3*d1) + c2*(b1*d3 - b3*d1) + 2*d2*(b1*c3 - b3*c1)) - uzx*(2*b2*(c1*d3 - c3*d1) + c2*(b1*d3 - b3*d1) - d2*(b1*c3 - b3*c1))) - Em2*L2*(3*b3*uzy*(b1*d2 - b2*d1) - 3*b3*uzz*(b1*c2 - b2*c1) + 3*d3*uxx*(c1*d2 - c2*d1) - 3*d3*uxy*(b1*d2 - b2*d1) + uxz*(-b3*(c1*d2 - c2*d1) + c3*(b1*d2 - b2*d1) + 2*d3*(b1*c2 - b2*c1)) - uzx*(2*b3*(c1*d2 - c2*d1) + c3*(b1*d2 - b2*d1) - d3*(b1*c2 - b2*c1))))
534
+ ez = -L1*(Em1*L1*(3*b2*uyy*(b1*d3 - b3*d1) - 3*b2*uyz*(b1*c3 - b3*c1) + 3*c2*uxx*(c1*d3 - c3*d1) + 3*c2*uxz*(b1*c3 - b3*c1) - uxy*(b2*(c1*d3 - c3*d1) + 2*c2*(b1*d3 - b3*d1) + d2*(b1*c3 - b3*c1)) - uyx*(2*b2*(c1*d3 - c3*d1) + c2*(b1*d3 - b3*d1) - d2*(b1*c3 - b3*c1))) - Em2*L2*(3*b3*uyy*(b1*d2 - b2*d1) - 3*b3*uyz*(b1*c2 - b2*c1) + 3*c3*uxx*(c1*d2 - c2*d1) + 3*c3*uxz*(b1*c2 - b2*c1) - uxy*(b3*(c1*d2 - c2*d1) + 2*c3*(b1*d2 - b2*d1) + d3*(b1*c2 - b2*c1)) - uyx*(2*b3*(c1*d2 - c2*d1) + c3*(b1*d2 - b2*d1) - d3*(b1*c2 - b2*c1))))
535
+
536
+ Exl += ex*V1
537
+ Eyl += ey*V1
538
+ Ezl += ez*V1
539
+
540
+ out = np.zeros((3,), dtype=np.complex128)
541
+ out[0] = Exl
542
+ out[1] = Eyl
543
+ out[2] = Ezl
544
+ return out
545
+
546
+ @njit(types.Tuple((f8[:], f8[:]))(f8[:,:], i8[:,:], i8[:,:], i8[:,:], f8[:,:],
547
+ c16[:], f8[:], f8[:], i8[:,:], i8[:,:],
548
+ f8[:,:], i8[:,:], i8[:,:], c16[:], c16[:], f8), cache=True, nogil=True)
549
+ def compute_error_single(nodes, tets, tris, edges, centers,
550
+ Efield,
551
+ edge_lengths,
552
+ areas,
553
+ tet_to_edge,
554
+ tet_to_tri,
555
+ tri_centers,
556
+ tri_to_tet,
557
+ tet_to_field,
558
+ er,
559
+ ur,
560
+ k0,) -> np.ndarray:
561
+
562
+
563
+ # UNPACK DATA
564
+ ntet = tets.shape[1]
565
+ nedges = edges.shape[1]
566
+
567
+
568
+ # INIT POSTERIORI ERROR ESTIMATE
569
+ error = np.zeros((ntet,), dtype=np.float64)
570
+ max_elem_size = np.zeros((ntet,), dtype=np.float64)
571
+
572
+ hks = np.zeros((ntet,), dtype=np.float64)
573
+ hfs = np.zeros((4,ntet), dtype=np.float64)
574
+ face_error1 = np.zeros((4,3,ntet), dtype=np.complex128)
575
+ face_error2 = np.zeros((4,3,ntet), dtype=np.complex128)
576
+
577
+
578
+ # Compute Error estimate
579
+ for itet in range(ntet):
580
+ uinv = (1/ur[itet])*np.eye(3)
581
+ ermat = er[itet]*np.eye(3)
582
+
583
+ vertices = nodes[:,tets[:, itet]]
584
+
585
+ g_node_ids = tets[:, itet]
586
+ g_edge_ids = edges[:, tet_to_field[:6, itet]]
587
+ g_tri_ids = tris[:, tet_to_field[6:10, itet]-nedges]
588
+
589
+ l_edge_ids = local_mapping(g_node_ids, g_edge_ids)
590
+ l_tri_ids = local_mapping(g_node_ids, g_tri_ids)
591
+
592
+ coords = centers[:,itet]
593
+ Ef = Efield[tet_to_field[:,itet]]
594
+ Rv1 = -compute_curl_curl(coords, vertices, Ef, l_edge_ids, l_tri_ids, uinv)
595
+ Rv2 = k0**2*(ermat @ compute_field(coords, vertices, Ef, l_edge_ids, l_tri_ids))
596
+
597
+ triids = tet_to_tri[:,itet]
598
+ facecoords = tri_centers[:, triids]
599
+
600
+ Volume = compute_volume(vertices[0,:], vertices[1,:], vertices[2,:])
601
+ hks[itet] = Volume**(1/3)
602
+ Rf = matmul(uinv, compute_curl(facecoords, vertices, Ef, l_edge_ids, l_tri_ids))
603
+ tetc = centers[:,itet].flatten()
604
+
605
+ max_elem_size[itet] = (Volume*12/np.sqrt(2))**(1/3)#(np.max(edge_lengths[tet_to_edge[:,itet]]) + np.min(edge_lengths[tet_to_edge[:,itet]]))/2
606
+
607
+ for iface in range(4):
608
+ i1, i2, i3 = tris[:, triids[iface]]
609
+ normal = outward_normal(nodes[:,i1], nodes[:,i2], nodes[:,i3], tetc).astype(np.complex128)
610
+
611
+ adj_tets = [int(tri_to_tet[j,triids[iface]]) for j in range(2)]
612
+ adj_tets = [num for num in adj_tets if num not in (itet, -1234)]
613
+
614
+ if len(adj_tets) == 0:
615
+ continue
616
+ area = areas[triids[iface]]
617
+
618
+ hfs[iface,itet] = area**0.5
619
+
620
+ itet_adj = adj_tets[0]
621
+ iface_adj = np.argwhere(tet_to_tri[:,itet_adj]==triids[iface])[0][0]
622
+
623
+ face_error2[iface_adj, :, itet_adj] = -area*cross_c(normal, uinv @ Rf[:, iface])
624
+ face_error1[iface, :, itet] = area*cross_c(normal, uinv @ Rf[:,iface])
625
+
626
+
627
+ error[itet] = np.linalg.norm(np.abs(Volume*(Rv1 + Rv2)))**2
628
+
629
+ fdiff = np.abs(face_error1 - face_error2)
630
+ fnorm = fdiff[:,0,:]**2 + fdiff[:,1,:]**2 + fdiff[:,2,:]**2
631
+ ferror = np.sum(fnorm*hfs, axis=0)
632
+ error = hks**2*error + 0.5*ferror
633
+
634
+ return error, max_elem_size
635
+
636
+ def compute_error_estimate(field: MWField) -> np.ndarray:
637
+ mesh = field.mesh
638
+
639
+ nodes = mesh.nodes
640
+ tris = mesh.tris
641
+ tets = mesh.tets
642
+ edges = mesh.edges
643
+ centers = mesh.centers
644
+
645
+ As = mesh.areas
646
+ tet_to_edge = mesh.tet_to_edge
647
+ tet_to_tri = mesh.tet_to_tri
648
+ tri_centers = mesh.tri_centers
649
+ tri_to_tet = mesh.tri_to_tet
650
+ tet_to_field = field.basis.tet_to_field
651
+ er = field._der
652
+ ur = field._dur
653
+ Ls = mesh.edge_lengths
654
+
655
+ errors = []
656
+ for key in field._fields.keys():
657
+ excitation = field._fields[key]
658
+
659
+ error, sizes = compute_error_single(nodes, tets, tris, edges,
660
+ centers, excitation, Ls, As,
661
+ tet_to_edge, tet_to_tri, tri_centers,
662
+ tri_to_tet, tet_to_field, er, ur, field.k0)
663
+
664
+ errors.append(error)
665
+
666
+ error = np.max(np.array(errors), axis=0)
667
+ return error, sizes
@@ -186,11 +186,12 @@ class Assembler:
186
186
 
187
187
  return E, B, np.array(solve_ids), nedlegfield
188
188
 
189
- def assemble_freq_matrix(self, field: Nedelec2,
190
- materials: list[Material],
191
- bcs: list[BoundaryCondition],
192
- frequency: float,
193
- cache_matrices: bool = False) -> SimJob:
189
+ def assemble_freq_matrix(self,
190
+ field: Nedelec2,
191
+ materials: list[Material],
192
+ bcs: list[BoundaryCondition],
193
+ frequency: float,
194
+ cache_matrices: bool = False) -> SimJob:
194
195
  """Assembles the frequency domain FEM matrix
195
196
 
196
197
  Args: