emerge 0.4.11__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.

@@ -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 scipy.sparse import csr_matrix, eye
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 collections import defaultdict
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
- def select_bc(bcs: list[BoundaryCondition], bctype):
38
- return [bc for bc in bcs if isinstance(bc,bctype)]
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
- mesh = field.mesh
195
- w0 = 2*np.pi*frequency
196
- k0 = w0/C0
231
+ # PREDEFINE CONSTANTS
232
+ W0 = 2*np.pi*frequency
233
+ K0 = W0/C0
197
234
 
198
- er = er - 1j*sig/(w0*EPS0)*np.repeat(np.eye(3)[:, :, np.newaxis], er.shape[2], axis=2)
199
-
200
- f_dependent_properties = np.any((sig > 0) & (sig < self.conductivity_limit))
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
- if cache_matrices and not f_dependent_properties and self.cached_matrices is not None:
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
- K: csr_matrix = (E - B*(k0**2)).tocsr()
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
- pecs: list[PEC] = [bc for bc in bcs if isinstance(bc,PEC)]
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
- ports: list[PortBC] = [bc for bc in bcs if isinstance(bc, PortBC)]
217
- periodic: list[Periodic] = [bc for bc in bcs if isinstance(bc, Periodic)]
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 ports}
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
- # PEC Boundary conditions
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(k0)
303
+ gamma = bc.get_gamma(K0)
265
304
 
266
305
  def Ufunc(x,y):
267
- return bc.get_Uinc(x,y,k0)
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(periodic) > 0:
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 periodic:
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
- trimapdict = defaultdict(lambda: [None, None])
300
- edgemapdict = defaultdict(lambda: [None, None])
301
- mult = int(10**(-np.round(np.log10(min(mesh.edge_lengths.flatten())))+3))
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
- for t1, t2 in trimapdict.values():
322
- f11, f21 = field.tri_to_field[[3,7],t1]
323
- f12, f22 = field.tri_to_field[[3,7],t2]
324
- Pmat[f12,f12] = 0
325
- Pmat[f22,f22] = 0
326
- if Pmat[f12,f11] == 0:
327
- Pmat[f12,f11] += phi
328
- else:
329
- Pmat[f12,f11] *= phi
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
- simjob = SimJob(K, b, k0*299792458/(2*np.pi), True)
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
- for t1, t2 in trimapdict.values():
502
- f11, f21 = field.tri_to_field[[3,7],t1]
503
- f12, f22 = field.tri_to_field[[3,7],t2]
504
- Pmat[f12,f12] = 0
505
- Pmat[f22,f22] = 0
506
- if Pmat[f12,f11] == 0:
507
- Pmat[f12,f11] += phi
508
- else:
509
- Pmat[f12,f11] *= phi
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
- # --- CACHED FACTORIALS ---------------
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
- # --- MAPPING FUNCTIONS ---------------
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
- # --- PRECOMPUTE VOLUME COEFFICINETS --------------------
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.debug(f'Precomputing group {i_group}.')
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.debug(f'Precomputing group {i_group}.')
684
+ logger.info(f'Precomputing group {i_group}.')
685
685
  jobs = []
686
686
  ## Assemble jobs
687
687
  for freq in fgroup: