emerge 0.4.10__py3-none-any.whl → 0.5.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.
- emerge/__init__.py +15 -8
- emerge/_emerge/bc.py +41 -2
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/pcb.py +49 -11
- emerge/_emerge/howto.py +2 -2
- emerge/_emerge/logsettings.py +83 -5
- emerge/_emerge/mesh3d.py +30 -12
- emerge/_emerge/mth/common_functions.py +28 -1
- emerge/_emerge/mth/integrals.py +25 -3
- emerge/_emerge/mth/optimized.py +126 -33
- emerge/_emerge/mth/pairing.py +97 -0
- emerge/_emerge/periodic.py +22 -0
- emerge/_emerge/physics/microwave/assembly/assembler.py +129 -155
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +35 -3
- emerge/_emerge/physics/microwave/assembly/periodicbc.py +130 -0
- emerge/_emerge/physics/microwave/microwave_3d.py +3 -3
- emerge/_emerge/physics/microwave/microwave_bc.py +5 -4
- emerge/_emerge/physics/microwave/microwave_data.py +2 -2
- emerge/_emerge/projects/_gen_base.txt +1 -1
- emerge/_emerge/simmodel.py +137 -126
- emerge/_emerge/solve_interfaces/pardiso_interface.py +468 -0
- emerge/_emerge/solver.py +104 -55
- emerge/lib.py +276 -41
- {emerge-0.4.10.dist-info → emerge-0.5.0.dist-info}/METADATA +2 -3
- {emerge-0.4.10.dist-info → emerge-0.5.0.dist-info}/RECORD +28 -25
- {emerge-0.4.10.dist-info → emerge-0.5.0.dist-info}/WHEEL +0 -0
- {emerge-0.4.10.dist-info → emerge-0.5.0.dist-info}/entry_points.txt +0 -0
- {emerge-0.4.10.dist-info → emerge-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,22 +20,25 @@ from ..microwave_bc import PEC, BoundaryCondition, RectangularWaveguide, RobinBC
|
|
|
20
20
|
from ....elements.nedelec2 import Nedelec2
|
|
21
21
|
from ....elements.nedleg2 import NedelecLegrange2
|
|
22
22
|
from ....mth.optimized import gaus_quad_tri
|
|
23
|
-
from
|
|
23
|
+
from ....mth.pairing import pair_coordinates
|
|
24
|
+
|
|
25
|
+
from scipy.sparse import csr_matrix
|
|
24
26
|
from loguru import logger
|
|
25
27
|
from ..simjob import SimJob
|
|
26
|
-
from
|
|
28
|
+
from .periodicbc import gen_periodic_matrix
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
############################################################
|
|
32
|
+
# CONSTANTS #
|
|
33
|
+
############################################################
|
|
27
34
|
|
|
28
35
|
C0 = 299792458
|
|
29
36
|
EPS0 = 8.854187818814e-12
|
|
30
37
|
|
|
31
|
-
def matprint(mat: np.ndarray) -> None:
|
|
32
|
-
factor = np.max(np.abs(mat.flatten()))
|
|
33
|
-
if factor == 0:
|
|
34
|
-
factor = 1
|
|
35
|
-
print(mat.real/factor)
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
############################################################
|
|
40
|
+
# FUNCTIONS #
|
|
41
|
+
############################################################
|
|
39
42
|
|
|
40
43
|
def diagnose_matrix(mat: np.ndarray) -> None:
|
|
41
44
|
|
|
@@ -56,10 +59,6 @@ def diagnose_matrix(mat: np.ndarray) -> None:
|
|
|
56
59
|
logger.error(f'Found large values at {ids}')
|
|
57
60
|
logger.info('Diagnostics finished')
|
|
58
61
|
|
|
59
|
-
#############
|
|
60
|
-
def gen_key(coord, mult):
|
|
61
|
-
return tuple([int(round(c*mult)) for c in coord])
|
|
62
|
-
|
|
63
62
|
def plane_basis_from_points(points: np.ndarray) -> np.ndarray:
|
|
64
63
|
"""
|
|
65
64
|
Compute an orthonormal basis from a cloud of 3D points dominantly
|
|
@@ -100,12 +99,23 @@ def plane_basis_from_points(points: np.ndarray) -> np.ndarray:
|
|
|
100
99
|
# Columns of eigvecs = principal axes
|
|
101
100
|
return eigvecs
|
|
102
101
|
|
|
102
|
+
|
|
103
|
+
############################################################
|
|
104
|
+
# THE ASSEMBLER CLASS #
|
|
105
|
+
############################################################
|
|
106
|
+
|
|
103
107
|
class Assembler:
|
|
108
|
+
"""The assembler class is responsible for FEM EM problem assembly.
|
|
104
109
|
|
|
110
|
+
It stores some cached properties to accellerate preformance.
|
|
111
|
+
"""
|
|
105
112
|
def __init__(self):
|
|
113
|
+
|
|
106
114
|
self.cached_matrices = None
|
|
107
115
|
self.conductivity_limit = 1e7
|
|
108
|
-
|
|
116
|
+
# Currently not used.
|
|
117
|
+
#self._Pmat_cache: dict[tuple[int,int], csr_matrix] = dict()
|
|
118
|
+
#self._remove_cache: list[int] = []
|
|
109
119
|
|
|
110
120
|
def assemble_bma_matrices(self,
|
|
111
121
|
field: Nedelec2,
|
|
@@ -115,14 +125,27 @@ class Assembler:
|
|
|
115
125
|
k0: float,
|
|
116
126
|
port: PortBC,
|
|
117
127
|
bcs: list[BoundaryCondition]) -> tuple[np.ndarray, np.ndarray, np.ndarray, NedelecLegrange2]:
|
|
118
|
-
|
|
128
|
+
"""Computes the boundary mode analysis matrices
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
field (Nedelec2): The Nedelec2 field object
|
|
132
|
+
er (np.ndarray): The relative permittivity tensor of shape (3,3,N)
|
|
133
|
+
ur (np.ndarray): The relative permeability tensor of shape (3,3,N)
|
|
134
|
+
sig (np.ndarray): The conductivity scalar of shape (N,)
|
|
135
|
+
k0 (float): The simulation phase constant
|
|
136
|
+
port (PortBC): The port boundary condition object
|
|
137
|
+
bcs (list[BoundaryCondition]): The other boundary conditions
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
tuple[np.ndarray, np.ndarray, np.ndarray, NedelecLegrange2]: The E, B, solve ids and Mixed order field object.
|
|
141
|
+
"""
|
|
119
142
|
from .generalized_eigen import generelized_eigenvalue_matrix
|
|
120
143
|
|
|
121
144
|
mesh = field.mesh
|
|
122
145
|
tri_ids = mesh.get_triangles(port.tags)
|
|
123
146
|
|
|
124
147
|
origin = tuple([c-n for c,n in zip(port.cs.origin, port.cs.gzhat)])
|
|
125
|
-
|
|
148
|
+
|
|
126
149
|
boundary_surface = mesh.boundary_surface(port.tags, origin)
|
|
127
150
|
nedlegfield = NedelecLegrange2(boundary_surface, port.cs)
|
|
128
151
|
|
|
@@ -187,49 +210,67 @@ class Assembler:
|
|
|
187
210
|
bcs: list[BoundaryCondition],
|
|
188
211
|
frequency: float,
|
|
189
212
|
cache_matrices: bool = False) -> SimJob:
|
|
190
|
-
|
|
213
|
+
"""Assembles the frequency domain FEM matrix
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
field (Nedelec2): The Nedelec2 object of the problems
|
|
217
|
+
er (np.ndarray): The relative dielectric permitivity tensor of shape (3,3,N)
|
|
218
|
+
ur (np.ndarray): The relative magnetic permeability tensor of shape (3,3,N)
|
|
219
|
+
sig (np.ndarray): The conductivity array of shape (N,)
|
|
220
|
+
bcs (list[BoundaryCondition]): The boundary conditions
|
|
221
|
+
frequency (float): The simulation frequency
|
|
222
|
+
cache_matrices (bool, optional): Whether to use and cache matrices. Defaults to False.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
SimJob: The resultant SimJob object
|
|
226
|
+
"""
|
|
227
|
+
|
|
191
228
|
from .curlcurl import tet_mass_stiffness_matrices
|
|
192
229
|
from .robinbc import assemble_robin_bc, assemble_robin_bc_excited
|
|
193
230
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
231
|
+
# PREDEFINE CONSTANTS
|
|
232
|
+
W0 = 2*np.pi*frequency
|
|
233
|
+
K0 = W0/C0
|
|
197
234
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
235
|
+
mesh = field.mesh
|
|
236
|
+
er = er - 1j*sig/(W0*EPS0)*np.repeat(np.eye(3)[:, :, np.newaxis], er.shape[2], axis=2)
|
|
237
|
+
is_frequency_dependent: bool = np.any((sig > 0) & (sig < self.conductivity_limit))
|
|
201
238
|
|
|
202
|
-
|
|
239
|
+
|
|
240
|
+
if cache_matrices and not is_frequency_dependent and self.cached_matrices is not None:
|
|
241
|
+
# IF CACHED AND AVAILABLE PULL E AND B FROM CACHE
|
|
203
242
|
logger.debug(' Retreiving cached matricies')
|
|
204
243
|
E, B = self.cached_matrices
|
|
205
244
|
else:
|
|
245
|
+
# OTHERWISE, COMPUTE
|
|
206
246
|
logger.debug(' Assembling matrices')
|
|
207
247
|
E, B = tet_mass_stiffness_matrices(field, er, ur)
|
|
208
248
|
self.cached_matrices = (E, B)
|
|
209
249
|
|
|
210
|
-
|
|
250
|
+
# COMBINE THE MASS AND STIFFNESS MATRIX
|
|
251
|
+
K: csr_matrix = (E - B*(K0**2)).tocsr()
|
|
211
252
|
|
|
212
253
|
NF = E.shape[0]
|
|
213
254
|
|
|
214
|
-
|
|
255
|
+
# ISOLATE BOUNDARY CONDITIONS TO ASSEMBLE
|
|
256
|
+
pec_bcs: list[PEC] = [bc for bc in bcs if isinstance(bc,PEC)]
|
|
215
257
|
robin_bcs: list[RectangularWaveguide] = [bc for bc in bcs if isinstance(bc,RobinBC)]
|
|
216
|
-
|
|
217
|
-
|
|
258
|
+
port_bcs: list[PortBC] = [bc for bc in bcs if isinstance(bc, PortBC)]
|
|
259
|
+
periodic_bcs: list[Periodic] = [bc for bc in bcs if isinstance(bc, Periodic)]
|
|
218
260
|
|
|
261
|
+
# PREDEFINE THE FORCING VECTOR CONTAINER
|
|
219
262
|
b = np.zeros((E.shape[0],)).astype(np.complex128)
|
|
220
|
-
port_vectors = {port.port_number: np.zeros((E.shape[0],)).astype(np.complex128) for port in
|
|
221
|
-
# Process all PEC Boundary Conditions
|
|
222
|
-
pec_ids = []
|
|
263
|
+
port_vectors = {port.port_number: np.zeros((E.shape[0],)).astype(np.complex128) for port in port_bcs}
|
|
223
264
|
|
|
265
|
+
# Process all PEC Boundary Conditions
|
|
224
266
|
logger.debug(' Implementing PEC Boundary Conditions.')
|
|
225
|
-
|
|
267
|
+
pec_ids = []
|
|
226
268
|
# Conductivity above al imit, consider it all PEC
|
|
227
269
|
for itet in range(field.n_tets):
|
|
228
270
|
if sig[itet] > self.conductivity_limit:
|
|
229
271
|
pec_ids.extend(field.tet_to_field[:,itet])
|
|
230
272
|
|
|
231
|
-
|
|
232
|
-
for pec in pecs:
|
|
273
|
+
for pec in pec_bcs:
|
|
233
274
|
if len(pec.tags)==0:
|
|
234
275
|
continue
|
|
235
276
|
face_tags = pec.tags
|
|
@@ -249,9 +290,7 @@ class Assembler:
|
|
|
249
290
|
logger.debug(' Implementing Robin Boundary Conditions.')
|
|
250
291
|
|
|
251
292
|
gauss_points = gaus_quad_tri(4)
|
|
252
|
-
|
|
253
293
|
Bempty = field.empty_tri_matrix()
|
|
254
|
-
|
|
255
294
|
for bc in robin_bcs:
|
|
256
295
|
|
|
257
296
|
for tag in bc.tags:
|
|
@@ -261,10 +300,10 @@ class Assembler:
|
|
|
261
300
|
nodes = mesh.get_nodes(face_tags)
|
|
262
301
|
edge_ids = list(mesh.tri_to_edge[:,tri_ids].flatten())
|
|
263
302
|
|
|
264
|
-
gamma = bc.get_gamma(
|
|
303
|
+
gamma = bc.get_gamma(K0)
|
|
265
304
|
|
|
266
305
|
def Ufunc(x,y):
|
|
267
|
-
return bc.get_Uinc(x,y,
|
|
306
|
+
return bc.get_Uinc(x,y,K0)
|
|
268
307
|
|
|
269
308
|
ibasis = bc.get_inv_basis()
|
|
270
309
|
if ibasis is None:
|
|
@@ -281,7 +320,7 @@ class Assembler:
|
|
|
281
320
|
B_p = field.generate_csr(Bempty)
|
|
282
321
|
K = K + B_p
|
|
283
322
|
|
|
284
|
-
if len(
|
|
323
|
+
if len(periodic_bcs) > 0:
|
|
285
324
|
logger.debug(' Implementing Periodic Boundary Conditions.')
|
|
286
325
|
|
|
287
326
|
|
|
@@ -290,75 +329,33 @@ class Assembler:
|
|
|
290
329
|
remove = set()
|
|
291
330
|
has_periodic = False
|
|
292
331
|
|
|
293
|
-
for bc in
|
|
294
|
-
Pmat = eye(NF, format='lil', dtype=np.complex128)
|
|
332
|
+
for bc in periodic_bcs:
|
|
295
333
|
has_periodic = True
|
|
296
334
|
tri_ids_1 = mesh.get_triangles(bc.face1.tags)
|
|
335
|
+
edge_ids_1 = mesh.get_edges(bc.face1.tags)
|
|
297
336
|
tri_ids_2 = mesh.get_triangles(bc.face2.tags)
|
|
337
|
+
edge_ids_2 = mesh.get_edges(bc.face2.tags)
|
|
298
338
|
dv = np.array(bc.dv)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
edge_ids_1 = set()
|
|
304
|
-
edge_ids_2 = set()
|
|
305
|
-
|
|
306
|
-
phi = bc.phi(k0)
|
|
307
|
-
for i1, i2 in zip(tri_ids_1, tri_ids_2):
|
|
308
|
-
trimapdict[gen_key(mesh.tri_centers[:,i1], mult)][0] = i1
|
|
309
|
-
trimapdict[gen_key(mesh.tri_centers[:,i2]-dv, mult)][1] = i2
|
|
310
|
-
|
|
311
|
-
ie1, ie2, ie3 = mesh.tri_to_edge[:,i1]
|
|
312
|
-
edge_ids_1.update({ie1, ie2, ie3})
|
|
313
|
-
ie1, ie2, ie3 = mesh.tri_to_edge[:,i2]
|
|
314
|
-
edge_ids_2.update({ie1, ie2, ie3})
|
|
315
|
-
|
|
316
|
-
for i1, i2 in zip(list(edge_ids_1), list(edge_ids_2)):
|
|
317
|
-
edgemapdict[gen_key(mesh.edge_centers[:,i1], mult)][0] = i1
|
|
318
|
-
edgemapdict[gen_key(mesh.edge_centers[:,i2]-dv, mult)][1] = i2
|
|
319
|
-
|
|
339
|
+
linked_tris = pair_coordinates(mesh.tri_centers, tri_ids_1, tri_ids_2, dv, 1e-9)
|
|
340
|
+
linked_edges = pair_coordinates(mesh.edge_centers, edge_ids_1, edge_ids_2, dv, 1e-9)
|
|
341
|
+
dv = np.array(bc.dv)
|
|
342
|
+
phi = bc.phi(K0)
|
|
320
343
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if Pmat[f22,f21] == 0:
|
|
332
|
-
Pmat[f22,f21] += phi
|
|
333
|
-
else:
|
|
334
|
-
Pmat[f22,f21] *= phi
|
|
335
|
-
remove.add(f12)
|
|
336
|
-
remove.add(f22)
|
|
337
|
-
|
|
338
|
-
for e1, e2 in edgemapdict.values():
|
|
339
|
-
f11, f21 = field.edge_to_field[:,e1]
|
|
340
|
-
f12, f22 = field.edge_to_field[:,e2]
|
|
341
|
-
Pmat[f12,f12] = 0
|
|
342
|
-
Pmat[f22,f22] = 0
|
|
343
|
-
if Pmat[f12,f11] == 0:
|
|
344
|
-
Pmat[f12,f11] += phi
|
|
345
|
-
else:
|
|
346
|
-
Pmat[f12,f11] *= phi
|
|
347
|
-
|
|
348
|
-
if Pmat[f22,f21] == 0:
|
|
349
|
-
Pmat[f22,f21] += phi
|
|
350
|
-
else:
|
|
351
|
-
Pmat[f22,f21] *= phi
|
|
352
|
-
remove.add(f12)
|
|
353
|
-
remove.add(f22)
|
|
354
|
-
|
|
344
|
+
Pmat, rows = gen_periodic_matrix(tri_ids_1,
|
|
345
|
+
edge_ids_1,
|
|
346
|
+
field.tri_to_field,
|
|
347
|
+
field.edge_to_field,
|
|
348
|
+
linked_tris,
|
|
349
|
+
linked_edges,
|
|
350
|
+
field.n_field,
|
|
351
|
+
phi)
|
|
352
|
+
remove.update(rows)
|
|
355
353
|
Pmats.append(Pmat)
|
|
356
354
|
|
|
357
355
|
if Pmats:
|
|
358
356
|
Pmat = Pmats[0]
|
|
359
357
|
for P2 in Pmats[1:]:
|
|
360
358
|
Pmat = Pmat @ P2
|
|
361
|
-
Pmat = Pmat.tocsr()
|
|
362
359
|
remove = np.array(sorted(list(remove)))
|
|
363
360
|
all_indices = np.arange(NF)
|
|
364
361
|
keep_indices = np.setdiff1d(all_indices, remove)
|
|
@@ -375,9 +372,10 @@ class Assembler:
|
|
|
375
372
|
mask = mask[keep_indices]
|
|
376
373
|
solve_ids = np.argwhere(mask==1).flatten()
|
|
377
374
|
|
|
378
|
-
logger.debug(f'Number of tets: {mesh.n_tets}')
|
|
379
|
-
logger.debug(f'Number of DoF: {K.shape[0]}')
|
|
380
|
-
|
|
375
|
+
logger.debug(f'Number of tets: {mesh.n_tets:,}')
|
|
376
|
+
logger.debug(f'Number of DoF: {K.shape[0]:,}')
|
|
377
|
+
logger.debug(f'Number of non-zero: {K.nnz:,}')
|
|
378
|
+
simjob = SimJob(K, b, K0*299792458/(2*np.pi), True)
|
|
381
379
|
|
|
382
380
|
simjob.port_vectors = port_vectors
|
|
383
381
|
simjob.solve_ids = solve_ids
|
|
@@ -395,7 +393,23 @@ class Assembler:
|
|
|
395
393
|
sig: np.ndarray,
|
|
396
394
|
bcs: list[BoundaryCondition],
|
|
397
395
|
frequency: float) -> SimJob:
|
|
398
|
-
|
|
396
|
+
"""Assembles the eigenmode analysis matrix
|
|
397
|
+
|
|
398
|
+
The assembly process is frequency dependent because the frequency-dependent properties
|
|
399
|
+
need a guess before solving. There is currently no adjustment after an eigenmode is found.
|
|
400
|
+
The frequency-dependent properties are simply calculated once for the given frequency
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
field (Nedelec2): The Nedelec2 field
|
|
404
|
+
er (np.ndarray): The relative permittivity tensor in shape (3,3,N)
|
|
405
|
+
ur (np.ndarray): The relative permeability tensor in shape (3,3,N)
|
|
406
|
+
sig (np.ndarray): The conductivity scalar in array (N,)
|
|
407
|
+
bcs (list[BoundaryCondition]): The list of boundary conditions
|
|
408
|
+
frequency (float): The compilation frequency (for material properties only)
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
SimJob: The resultant simulation job
|
|
412
|
+
"""
|
|
399
413
|
from .curlcurl import tet_mass_stiffness_matrices
|
|
400
414
|
from .robinbc import assemble_robin_bc
|
|
401
415
|
|
|
@@ -472,66 +486,26 @@ class Assembler:
|
|
|
472
486
|
has_periodic = False
|
|
473
487
|
|
|
474
488
|
for bc in periodic:
|
|
475
|
-
Pmat = eye(NF, format='lil', dtype=np.complex128)
|
|
476
489
|
has_periodic = True
|
|
477
490
|
tri_ids_1 = mesh.get_triangles(bc.face1.tags)
|
|
491
|
+
edge_ids_1 = mesh.get_edges(bc.face1.tags)
|
|
478
492
|
tri_ids_2 = mesh.get_triangles(bc.face2.tags)
|
|
493
|
+
edge_ids_2 = mesh.get_edges(bc.face2.tags)
|
|
494
|
+
dv = np.array(bc.dv)
|
|
495
|
+
linked_tris = pair_coordinates(mesh.tri_centers, tri_ids_1, tri_ids_2, dv, 1e-9)
|
|
496
|
+
linked_edges = pair_coordinates(mesh.edge_centers, edge_ids_1, edge_ids_2, dv, 1e-9)
|
|
479
497
|
dv = np.array(bc.dv)
|
|
480
|
-
trimapdict = defaultdict(lambda: [None, None])
|
|
481
|
-
edgemapdict = defaultdict(lambda: [None, None])
|
|
482
|
-
mult = int(10**(-np.round(np.ceil(min(mesh.edge_lengths.flatten())))+2))
|
|
483
|
-
edge_ids_1 = set()
|
|
484
|
-
edge_ids_2 = set()
|
|
485
|
-
|
|
486
498
|
phi = bc.phi(k0)
|
|
487
|
-
for i1, i2 in zip(tri_ids_1, tri_ids_2):
|
|
488
|
-
trimapdict[gen_key(mesh.tri_centers[:,i1], mult)][0] = i1
|
|
489
|
-
trimapdict[gen_key(mesh.tri_centers[:,i2]-dv, mult)][1] = i2
|
|
490
|
-
|
|
491
|
-
ie1, ie2, ie3 = mesh.tri_to_edge[:,i1]
|
|
492
|
-
edge_ids_1.update({ie1, ie2, ie3})
|
|
493
|
-
ie1, ie2, ie3 = mesh.tri_to_edge[:,i2]
|
|
494
|
-
edge_ids_2.update({ie1, ie2, ie3})
|
|
495
|
-
|
|
496
|
-
for i1, i2 in zip(list(edge_ids_1), list(edge_ids_2)):
|
|
497
|
-
edgemapdict[gen_key(mesh.edge_centers[:,i1], mult)][0] = i1
|
|
498
|
-
edgemapdict[gen_key(mesh.edge_centers[:,i2]-dv, mult)][1] = i2
|
|
499
|
-
|
|
500
499
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if Pmat[f22,f21] == 0:
|
|
512
|
-
Pmat[f22,f21] += phi
|
|
513
|
-
else:
|
|
514
|
-
Pmat[f22,f21] *= phi
|
|
515
|
-
remove.add(f12)
|
|
516
|
-
remove.add(f22)
|
|
517
|
-
|
|
518
|
-
for e1, e2 in edgemapdict.values():
|
|
519
|
-
f11, f21 = field.edge_to_field[:,e1]
|
|
520
|
-
f12, f22 = field.edge_to_field[:,e2]
|
|
521
|
-
Pmat[f12,f12] = 0
|
|
522
|
-
Pmat[f22,f22] = 0
|
|
523
|
-
if Pmat[f12,f11] == 0:
|
|
524
|
-
Pmat[f12,f11] += phi
|
|
525
|
-
else:
|
|
526
|
-
Pmat[f12,f11] *= phi
|
|
527
|
-
|
|
528
|
-
if Pmat[f22,f21] == 0:
|
|
529
|
-
Pmat[f22,f21] += phi
|
|
530
|
-
else:
|
|
531
|
-
Pmat[f22,f21] *= phi
|
|
532
|
-
remove.add(f12)
|
|
533
|
-
remove.add(f22)
|
|
534
|
-
|
|
500
|
+
Pmat, rows = gen_periodic_matrix(tri_ids_1,
|
|
501
|
+
edge_ids_1,
|
|
502
|
+
field.tri_to_field,
|
|
503
|
+
field.edge_to_field,
|
|
504
|
+
linked_tris,
|
|
505
|
+
linked_edges,
|
|
506
|
+
field.n_field,
|
|
507
|
+
phi)
|
|
508
|
+
remove.update(rows)
|
|
535
509
|
Pmats.append(Pmat)
|
|
536
510
|
|
|
537
511
|
if Pmats:
|
|
@@ -22,12 +22,20 @@ from numba_progress import ProgressBar, ProgressBarType
|
|
|
22
22
|
from ....mth.optimized import local_mapping, matinv, dot_c, cross_c, compute_distances
|
|
23
23
|
from numba import c16, types, f8, i8, njit, prange
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
############################################################
|
|
27
|
+
# CACHED FACTORIAL VALUES #
|
|
28
|
+
############################################################
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
_FACTORIALS = np.array([1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880], dtype=np.int64)
|
|
28
32
|
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
############################################################
|
|
36
|
+
# INDEX MAPPING FUNCTIONS #
|
|
37
|
+
############################################################
|
|
38
|
+
|
|
31
39
|
# These mapping functions return edge and face coordinates in the appropriate order.
|
|
32
40
|
|
|
33
41
|
@njit(i8[:,:](i8[:,:], i8[:,:], i8[:,:], i8, i8), cache=True, nogil=True)
|
|
@@ -73,7 +81,11 @@ def volume_coeff(a: int, b: int, c: int, d: int):
|
|
|
73
81
|
*_FACTORIALS[klmn[4]]*_FACTORIALS[klmn[5]]*_FACTORIALS[klmn[6]])/_FACTORIALS[(np.sum(klmn[1:])+3)]
|
|
74
82
|
return output
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
|
|
85
|
+
############################################################
|
|
86
|
+
# PRECOMPUTATION OF INTEGRATION COEFFICIENTS #
|
|
87
|
+
############################################################
|
|
88
|
+
|
|
77
89
|
NFILL = 5
|
|
78
90
|
VOLUME_COEFF_CACHE_BASE = np.zeros((NFILL,NFILL,NFILL,NFILL), dtype=np.float64)
|
|
79
91
|
for I in range(NFILL):
|
|
@@ -84,6 +96,11 @@ for I in range(NFILL):
|
|
|
84
96
|
|
|
85
97
|
VOLUME_COEFF_CACHE = VOLUME_COEFF_CACHE_BASE
|
|
86
98
|
|
|
99
|
+
|
|
100
|
+
############################################################
|
|
101
|
+
# COMPUTATION OF THE BARYCENTRIC COORDINATE COEFFICIENTS #
|
|
102
|
+
############################################################
|
|
103
|
+
|
|
87
104
|
@njit(types.Tuple((f8[:], f8[:], f8[:], f8))(f8[:], f8[:], f8[:]), cache = True, nogil=True)
|
|
88
105
|
def tet_coefficients_bcd(xs: np.ndarray, ys: np.ndarray, zs: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray, float]:
|
|
89
106
|
"""Computes the a,b,c and d coefficients of a tet barycentric coordinate functions and the volume
|
|
@@ -125,6 +142,11 @@ def tet_coefficients_bcd(xs: np.ndarray, ys: np.ndarray, zs: np.ndarray) -> tupl
|
|
|
125
142
|
|
|
126
143
|
return bbs, ccs, dds, V
|
|
127
144
|
|
|
145
|
+
|
|
146
|
+
############################################################
|
|
147
|
+
# MAIN CURL-CURL MATRIX ASSEMBLY #
|
|
148
|
+
############################################################
|
|
149
|
+
|
|
128
150
|
def tet_mass_stiffness_matrices(field: Nedelec2,
|
|
129
151
|
er: np.ndarray,
|
|
130
152
|
ur: np.ndarray) -> tuple[csr_matrix, csr_matrix]:
|
|
@@ -157,6 +179,11 @@ def tet_mass_stiffness_matrices(field: Nedelec2,
|
|
|
157
179
|
|
|
158
180
|
return E, B
|
|
159
181
|
|
|
182
|
+
|
|
183
|
+
############################################################
|
|
184
|
+
# NUMBA ACCELLERATE SUB-MATRIX ASSEMBLY #
|
|
185
|
+
############################################################
|
|
186
|
+
|
|
160
187
|
@njit(types.Tuple((c16[:,:],c16[:,:]))(f8[:,:], f8[:], i8[:,:], i8[:,:], c16[:,:], c16[:,:]), nogil=True, cache=True, parallel=False, fastmath=True)
|
|
161
188
|
def ned2_tet_stiff_mass(tet_vertices, edge_lengths, local_edge_map, local_tri_map, Ms, Mm):
|
|
162
189
|
''' Nedelec 2 tetrahedral stiffness and mass matrix submatrix Calculation
|
|
@@ -393,6 +420,11 @@ def ned2_tet_stiff_mass(tet_vertices, edge_lengths, local_edge_map, local_tri_ma
|
|
|
393
420
|
|
|
394
421
|
return Dmat, Fmat
|
|
395
422
|
|
|
423
|
+
|
|
424
|
+
############################################################
|
|
425
|
+
# NUMBA ACCELLERATED MATRIX BUILDER #
|
|
426
|
+
############################################################
|
|
427
|
+
|
|
396
428
|
@njit(types.Tuple((c16[:], c16[:], i8[:], i8[:]))(f8[:,:],
|
|
397
429
|
i8[:,:],
|
|
398
430
|
i8[:,:],
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
from numba import njit, types, i8, c16
|
|
21
|
+
from scipy.sparse import csr_matrix
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
############################################################
|
|
25
|
+
# NUMBA COMPILED #
|
|
26
|
+
############################################################
|
|
27
|
+
|
|
28
|
+
@njit(types.Tuple((i8[:], i8[:], c16[:], c16[:]))(i8[:,:], i8[:,:], i8[:,:], i8[:,:], i8), cache=True, nogil=True)
|
|
29
|
+
def _fill_periodic_matrix(tris: np.ndarray, edges: np.ndarray, tri_to_field: np.ndarray, edge_to_field: np.ndarray, Nfield: int) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
30
|
+
"""Generates sparse matrix row, column ids and ones plus the matrix diagonal.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
tris (: np.ndarray): The triangle ids
|
|
34
|
+
edges (: np.ndarray): The edge ids
|
|
35
|
+
tri_to_field (: np.ndarray): The triangle to field index mapping
|
|
36
|
+
edge_to_field (: np.ndarray): The edge to field index mapping
|
|
37
|
+
Nfield (int): The number of field points
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
np.ndarray: The row ids
|
|
41
|
+
np.ndarray: The column ids
|
|
42
|
+
np.ndarray: The data
|
|
43
|
+
np.ndarray: The diagonal array
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# NUMBERS
|
|
47
|
+
N = tris.shape[1] + edges.shape[1]
|
|
48
|
+
NT = tris.shape[1]
|
|
49
|
+
|
|
50
|
+
DIAGONAL = np.ones((Nfield,), dtype=np.complex128)
|
|
51
|
+
ROWS = np.zeros((N*2,), dtype=np.int64)
|
|
52
|
+
COLS = np.zeros((N*2,), dtype=np.int64)
|
|
53
|
+
TERMS = np.zeros((N*2,), dtype=np.complex128)
|
|
54
|
+
|
|
55
|
+
i = 0
|
|
56
|
+
for it in range(NT):
|
|
57
|
+
t1 = tris[0,it]
|
|
58
|
+
t2 = tris[1,it]
|
|
59
|
+
f11 = tri_to_field[3, t1]
|
|
60
|
+
f21 = tri_to_field[7, t1]
|
|
61
|
+
f12 = tri_to_field[3, t2]
|
|
62
|
+
f22 = tri_to_field[7, t2]
|
|
63
|
+
DIAGONAL[f12] = 0.
|
|
64
|
+
DIAGONAL[f22] = 0.
|
|
65
|
+
ROWS[i] = f12
|
|
66
|
+
ROWS[i+1] = f22
|
|
67
|
+
COLS[i] = f11
|
|
68
|
+
COLS[i+1] = f21
|
|
69
|
+
TERMS[i] = 1.0
|
|
70
|
+
TERMS[i+1] = 1.0
|
|
71
|
+
i += 2
|
|
72
|
+
NE = edges.shape[1]
|
|
73
|
+
for ie in range(NE):
|
|
74
|
+
e1 = edges[0,ie]
|
|
75
|
+
e2 = edges[1,ie]
|
|
76
|
+
f11 = edge_to_field[0, e1]
|
|
77
|
+
f21 = edge_to_field[1, e1]
|
|
78
|
+
f12 = edge_to_field[0, e2]
|
|
79
|
+
f22 = edge_to_field[1, e2]
|
|
80
|
+
DIAGONAL[f12] = 0.
|
|
81
|
+
DIAGONAL[f22] = 0.
|
|
82
|
+
ROWS[i] = f12
|
|
83
|
+
ROWS[i+1] = f22
|
|
84
|
+
COLS[i] = f11
|
|
85
|
+
COLS[i+1] = f21
|
|
86
|
+
TERMS[i] = 1.0
|
|
87
|
+
TERMS[i+1] = 1.0
|
|
88
|
+
i += 2
|
|
89
|
+
ROWS = ROWS[:i]
|
|
90
|
+
COLS = COLS[:i]
|
|
91
|
+
TERMS = TERMS[:i]
|
|
92
|
+
return ROWS, COLS, TERMS, DIAGONAL
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
############################################################
|
|
96
|
+
# PYTHON INTERFACE #
|
|
97
|
+
############################################################
|
|
98
|
+
|
|
99
|
+
def gen_periodic_matrix(tris: np.ndarray,
|
|
100
|
+
edges: np.ndarray,
|
|
101
|
+
tri_to_field: np.ndarray,
|
|
102
|
+
edge_to_field: np.ndarray,
|
|
103
|
+
linked_tris: dict[int, int],
|
|
104
|
+
linked_edges: dict[int, int],
|
|
105
|
+
Nfield: int,
|
|
106
|
+
phi: complex) -> tuple[csr_matrix, np.ndarray]:
|
|
107
|
+
"""This function constructs the periodic boundary matrix
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
tris (np.ndarray): _description_
|
|
111
|
+
edges (np.ndarray): _description_
|
|
112
|
+
tri_to_field (np.ndarray): _description_
|
|
113
|
+
edge_to_field (np.ndarray): _description_
|
|
114
|
+
linked_tris (dict[int, int]): _description_
|
|
115
|
+
linked_edges (dict[int, int]): _description_
|
|
116
|
+
Nfield (int): _description_
|
|
117
|
+
phi (complex): _description_
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
tuple[csr_matrix, np.ndarray]: _description_
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
tris_array = np.array([(tri, linked_tris[tri]) for tri in tris]).T
|
|
124
|
+
edges_array = np.array([(edge, linked_edges[edge]) for edge in edges]).T
|
|
125
|
+
ROWS, COLS, TERMS, diagonal = _fill_periodic_matrix(tris_array, edges_array, tri_to_field, edge_to_field, Nfield)
|
|
126
|
+
matrix = csr_matrix((TERMS, (ROWS, COLS)), [Nfield, Nfield], dtype=np.complex128)
|
|
127
|
+
matrix.data.fill(phi)
|
|
128
|
+
matrix.setdiag(diagonal)
|
|
129
|
+
|
|
130
|
+
return matrix, ROWS
|
|
@@ -176,9 +176,9 @@ class Microwave3D:
|
|
|
176
176
|
logger.debug('Initializing boundary conditions.')
|
|
177
177
|
|
|
178
178
|
tags = self.mesher.domain_boundary_face_tags
|
|
179
|
+
tags = [tag for tag in tags if tag not in self.bc.assigned(2)]
|
|
179
180
|
self.bc.PEC(FaceSelection(tags))
|
|
180
181
|
logger.info(f'Adding PEC boundary condition with tags {tags}.')
|
|
181
|
-
|
|
182
182
|
if self.mesher.periodic_cell is not None:
|
|
183
183
|
self.mesher.periodic_cell.generate_bcs()
|
|
184
184
|
for bc in self.mesher.periodic_cell.bcs:
|
|
@@ -656,7 +656,7 @@ class Microwave3D:
|
|
|
656
656
|
# ITERATE OVER FREQUENCIES
|
|
657
657
|
freq_groups
|
|
658
658
|
for i_group, fgroup in enumerate(freq_groups):
|
|
659
|
-
logger.
|
|
659
|
+
logger.info(f'Precomputing group {i_group}.')
|
|
660
660
|
jobs = []
|
|
661
661
|
## Assemble jobs
|
|
662
662
|
for ifreq, freq in enumerate(fgroup):
|
|
@@ -681,7 +681,7 @@ class Microwave3D:
|
|
|
681
681
|
with ThreadPoolExecutor(max_workers=njobs) as executor:
|
|
682
682
|
# ITERATE OVER FREQUENCIES
|
|
683
683
|
for i_group, fgroup in enumerate(freq_groups):
|
|
684
|
-
logger.
|
|
684
|
+
logger.info(f'Precomputing group {i_group}.')
|
|
685
685
|
jobs = []
|
|
686
686
|
## Assemble jobs
|
|
687
687
|
for freq in fgroup:
|