emerge 0.4.7__py3-none-any.whl → 0.4.8__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.

Files changed (78) hide show
  1. emerge/__init__.py +14 -14
  2. emerge/_emerge/__init__.py +42 -0
  3. emerge/_emerge/bc.py +197 -0
  4. emerge/_emerge/coord.py +119 -0
  5. emerge/_emerge/cs.py +523 -0
  6. emerge/_emerge/dataset.py +36 -0
  7. emerge/_emerge/elements/__init__.py +19 -0
  8. emerge/_emerge/elements/femdata.py +212 -0
  9. emerge/_emerge/elements/index_interp.py +64 -0
  10. emerge/_emerge/elements/legrange2.py +172 -0
  11. emerge/_emerge/elements/ned2_interp.py +645 -0
  12. emerge/_emerge/elements/nedelec2.py +140 -0
  13. emerge/_emerge/elements/nedleg2.py +217 -0
  14. emerge/_emerge/geo/__init__.py +24 -0
  15. emerge/_emerge/geo/horn.py +107 -0
  16. emerge/_emerge/geo/modeler.py +449 -0
  17. emerge/_emerge/geo/operations.py +254 -0
  18. emerge/_emerge/geo/pcb.py +1244 -0
  19. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  20. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  21. emerge/_emerge/geo/pmlbox.py +204 -0
  22. emerge/_emerge/geo/polybased.py +529 -0
  23. emerge/_emerge/geo/shapes.py +427 -0
  24. emerge/_emerge/geo/step.py +77 -0
  25. emerge/_emerge/geo2d.py +86 -0
  26. emerge/_emerge/geometry.py +510 -0
  27. emerge/_emerge/howto.py +214 -0
  28. emerge/_emerge/logsettings.py +5 -0
  29. emerge/_emerge/material.py +118 -0
  30. emerge/_emerge/mesh3d.py +730 -0
  31. emerge/_emerge/mesher.py +339 -0
  32. emerge/_emerge/mth/common_functions.py +33 -0
  33. emerge/_emerge/mth/integrals.py +71 -0
  34. emerge/_emerge/mth/optimized.py +357 -0
  35. emerge/_emerge/periodic.py +263 -0
  36. emerge/_emerge/physics/__init__.py +0 -0
  37. emerge/_emerge/physics/microwave/__init__.py +1 -0
  38. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  39. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  40. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  41. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  42. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  43. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  44. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  45. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  46. emerge/_emerge/physics/microwave/periodic.py +82 -0
  47. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  48. emerge/_emerge/physics/microwave/sc.py +175 -0
  49. emerge/_emerge/physics/microwave/simjob.py +147 -0
  50. emerge/_emerge/physics/microwave/sparam.py +138 -0
  51. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  52. emerge/_emerge/plot/__init__.py +0 -0
  53. emerge/_emerge/plot/display.py +394 -0
  54. emerge/_emerge/plot/grapher.py +93 -0
  55. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  56. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  57. emerge/_emerge/plot/pyvista/display.py +931 -0
  58. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  59. emerge/_emerge/plot/simple_plots.py +551 -0
  60. emerge/_emerge/plot.py +225 -0
  61. emerge/_emerge/projects/__init__.py +0 -0
  62. emerge/_emerge/projects/_gen_base.txt +32 -0
  63. emerge/_emerge/projects/_load_base.txt +24 -0
  64. emerge/_emerge/projects/generate_project.py +40 -0
  65. emerge/_emerge/selection.py +596 -0
  66. emerge/_emerge/simmodel.py +444 -0
  67. emerge/_emerge/simulation_data.py +411 -0
  68. emerge/_emerge/solver.py +993 -0
  69. emerge/_emerge/system.py +54 -0
  70. emerge/cli.py +19 -0
  71. emerge/lib.py +1 -1
  72. emerge/plot.py +1 -1
  73. {emerge-0.4.7.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
  74. emerge-0.4.8.dist-info/RECORD +78 -0
  75. emerge-0.4.8.dist-info/entry_points.txt +2 -0
  76. emerge-0.4.7.dist-info/RECORD +0 -9
  77. emerge-0.4.7.dist-info/entry_points.txt +0 -2
  78. {emerge-0.4.7.dist-info → emerge-0.4.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1150 @@
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 ...mesher import Mesher
19
+ from ...material import Material
20
+ from ...mesh3d import Mesh3D
21
+ from ...coord import Line
22
+ from ...elements.femdata import FEMBasis
23
+ from ...solver import DEFAULT_ROUTINE, SolveRoutine
24
+ from ...system import called_from_main_function
25
+ from ...selection import FaceSelection
26
+ from scipy.sparse.linalg import inv as sparse_inverse
27
+ from .microwave_bc import MWBoundaryConditionSet, PEC, ModalPort, LumpedPort, PortBC
28
+ from .microwave_data import MWData
29
+ from .assembly.assembler import Assembler
30
+ from .port_functions import compute_avg_power_flux
31
+ from .simjob import SimJob
32
+ from concurrent.futures import ThreadPoolExecutor
33
+ import numpy as np
34
+ from loguru import logger
35
+ from typing import Callable, Literal
36
+ import time
37
+ import threading
38
+ import multiprocessing as mp
39
+ from cmath import sqrt as csqrt
40
+
41
+ class SimulationError(Exception):
42
+ pass
43
+
44
+ def run_job_multi(job: SimJob) -> SimJob:
45
+ """The job launcher for Multi-Processing environements
46
+
47
+ Args:
48
+ job (SimJob): The Simulation Job
49
+
50
+ Returns:
51
+ SimJob: The solved SimJob
52
+ """
53
+ routine = DEFAULT_ROUTINE.duplicate().configure('MP')
54
+ for A, b, ids, reuse in job.iter_Ab():
55
+ solution, report = routine.solve(A, b, ids, reuse, id=job.id)
56
+ job.submit_solution(solution, report)
57
+ return job
58
+
59
+
60
+ def _dimstring(data: list[float]) -> str:
61
+ """A String formatter for dimensions in millimeters
62
+
63
+ Args:
64
+ data (list[float]): The list of floating point dimensions
65
+
66
+ Returns:
67
+ str: The formatted string
68
+ """
69
+ return '(' + ', '.join([f'{x*1000:.1f}mm' for x in data]) + ')'
70
+
71
+ def shortest_path(xyz1: np.ndarray, xyz2: np.ndarray, Npts: int) -> np.ndarray:
72
+ """
73
+ Finds the pair of points (one from xyz1, one from xyz2) that are closest in Euclidean distance,
74
+ and returns a (3, Npts) array of points linearly interpolating between them.
75
+
76
+ Parameters:
77
+ xyz1 : np.ndarray of shape (3, N1)
78
+ xyz2 : np.ndarray of shape (3, N2)
79
+ Npts : int, number of points in the output path
80
+
81
+ Returns:
82
+ np.ndarray of shape (3, Npts)
83
+ """
84
+ # Compute pairwise distances (N1 x N2)
85
+ diffs = xyz1[:, :, np.newaxis] - xyz2[:, np.newaxis, :]
86
+ dists = np.linalg.norm(diffs, axis=0) # shape (N1, N2)
87
+
88
+ # Find indices of closest pair
89
+ i1, i2 = np.unravel_index(np.argmin(dists), dists.shape)
90
+ p1 = xyz1[:, i1]
91
+ p2 = xyz2[:, i2]
92
+
93
+ # Interpolate linearly between p1 and p2
94
+ t = np.linspace(0, 1, Npts)
95
+ path = (1 - t) * p1[:, np.newaxis] + t * p2[:, np.newaxis]
96
+
97
+ return path
98
+
99
+ class Microwave3D:
100
+ """The Electrodynamics time harmonic physics class.
101
+
102
+ This class contains all physics dependent features to perform EM simuation in the time-harmonic
103
+ formulation.
104
+
105
+ """
106
+ def __init__(self, mesher: Mesher, mwdata: MWData, order: int = 2):
107
+ self.frequencies: list[float] = []
108
+ self.current_frequency = 0
109
+ self.order: int = order
110
+ self.resolution: float = 1
111
+
112
+ self.mesher: Mesher = mesher
113
+ self.mesh: Mesh3D = None
114
+
115
+ self.assembler: Assembler = Assembler()
116
+ self.bc: MWBoundaryConditionSet = MWBoundaryConditionSet(None)
117
+ self.basis: FEMBasis = None
118
+ self.solveroutine: SolveRoutine = DEFAULT_ROUTINE
119
+ self.set_order(order)
120
+ self.cache_matrices: bool = True
121
+
122
+ ## States
123
+ self._bc_initialized: bool = False
124
+ self.data: MWData = mwdata
125
+
126
+ ## Data
127
+ self._params: dict[str, float] = dict()
128
+ self._simstart: int = 0
129
+ self._simend: int = 0
130
+
131
+ def reset_data(self):
132
+ self.data = MWData()
133
+
134
+ def reset(self):
135
+ self.bc.reset()
136
+ self.basis: FEMBasis = None
137
+ self.bc = MWBoundaryConditionSet(None)
138
+ self.solveroutine.reset()
139
+
140
+ def set_order(self, order: int) -> None:
141
+ """Sets the order of the basis functions used. Currently only supports second order.
142
+
143
+ Args:
144
+ order (int): The order to use.
145
+
146
+ Raises:
147
+ ValueError: An error if a wrong order is used.
148
+ """
149
+ if order not in (2,):
150
+ raise ValueError(f'Order {order} not supported. Only order-2 allowed.')
151
+
152
+ self.order = order
153
+ self.resolution = {1: 0.15, 2: 0.3}[order]
154
+
155
+ @property
156
+ def nports(self) -> int:
157
+ """The number of ports in the physics.
158
+
159
+ Returns:
160
+ int: The number of ports
161
+ """
162
+ return self.bc.count(PortBC)
163
+
164
+ def ports(self) -> list[PortBC]:
165
+ """A list of all port boundary conditions.
166
+
167
+ Returns:
168
+ list[PortBC]: A list of all port boundary conditions
169
+ """
170
+ return sorted(self.bc.oftype(PortBC), key=lambda x: x.number)
171
+
172
+
173
+ def _initialize_bcs(self) -> None:
174
+ """Initializes the boundary conditions to set PEC as all exterior boundaries.
175
+ """
176
+ logger.debug('Initializing boundary conditions.')
177
+
178
+ tags = self.mesher.domain_boundary_face_tags
179
+ self.bc.PEC(FaceSelection(tags))
180
+ logger.info(f'Adding PEC boundary condition with tags {tags}.')
181
+
182
+ if self.mesher.periodic_cell is not None:
183
+ self.mesher.periodic_cell.generate_bcs()
184
+ for bc in self.mesher.periodic_cell.bcs:
185
+ self.bc.assign(bc)
186
+
187
+ def set_frequency(self, frequency: float | list[float] | np.ndarray ) -> None:
188
+ """Define the frequencies for the frequency sweep
189
+
190
+ Args:
191
+ frequency (float | list[float] | np.ndarray): The frequency points.
192
+ """
193
+ logger.info(f'Setting frequency as {frequency/1e6}MHz.')
194
+ if isinstance(frequency, (tuple, list, np.ndarray)):
195
+ self.frequencies = list(frequency)
196
+ else:
197
+ self.frequencies = [frequency]
198
+
199
+ self.mesher.max_size = self.resolution * 299792458 / max(self.frequencies)
200
+ self.mesher.min_size = 0.1 * self.mesher.max_size
201
+
202
+ logger.debug(f'Setting global mesh size range to: {self.mesher.min_size*1000:.3f}mm - {self.mesher.max_size*1000:.3f}mm')
203
+
204
+ def set_frequency_range(self, fmin: float, fmax: float, Npoints: int) -> None:
205
+ """Set the frequency range using the np.linspace syntax
206
+
207
+ Args:
208
+ fmin (float): The starting frequency
209
+ fmax (float): The ending frequency
210
+ Npoints (int): The number of points
211
+ """
212
+ self.set_frequency(np.linspace(fmin, fmax, Npoints))
213
+
214
+ def fdense(self, Npoints: int) -> np.ndarray:
215
+ if len(self.frequencies) == 1:
216
+ raise ValueError('Only 1 frequency point known. At least two need to be defined.')
217
+ fmin = min(self.frequencies)
218
+ fmax = max(self.frequencies)
219
+ return np.linspace(fmin, fmax, Npoints)
220
+
221
+ def set_resolution(self, resolution: float) -> None:
222
+ """Define the simulation resolution as the fraction of the wavelength.
223
+
224
+ To define the wavelength as ¼λ, call .set_resolution(0.25)
225
+
226
+ Args:
227
+ resolution (float): The desired wavelength fraction.
228
+
229
+ """
230
+ self.resolution = resolution
231
+
232
+ def set_conductivity_limit(self, condutivity: float) -> None:
233
+ """Sets the limit of a material conductivity value beyond which
234
+ the assembler considers it PEC. By default this value is
235
+ set to 1·10⁷S/m which means copper conductivity is ignored.
236
+
237
+ Args:
238
+ condutivity (float): The conductivity level in S/m
239
+ """
240
+ if condutivity < 0:
241
+ raise ValueError('Conductivity values must be above 0. Ignoring assignment')
242
+
243
+ self.assembler.conductivity_limit = condutivity
244
+
245
+ def get_discretizer(self) -> Callable:
246
+ """Returns a discretizer function that defines the maximum mesh size.
247
+
248
+ Returns:
249
+ Callable: The discretizer function
250
+ """
251
+ def disc(material: Material):
252
+ return 299792458/(max(self.frequencies) * np.real(material.neff))
253
+ return disc
254
+
255
+ def _initialize_field(self):
256
+ """Initializes the physics basis to the correct FEMBasis object.
257
+
258
+ Currently it defaults to Nedelec2. Mixed basis are used for modal analysis.
259
+ This function does not have to be called by the user. Its automatically invoked.
260
+ """
261
+ if self.basis is not None:
262
+ return
263
+ if self.order == 1:
264
+ raise NotImplementedError('Nedelec 1 is temporarily not supported')
265
+ from ...elements import Nedelec1
266
+ self.basis = Nedelec1(self.mesh)
267
+ elif self.order == 2:
268
+ from ...elements.nedelec2 import Nedelec2
269
+ self.basis = Nedelec2(self.mesh)
270
+
271
+ def _initialize_bc_data(self):
272
+ ''' Initializes auxilliary required boundary condition information before running simulations.
273
+ '''
274
+ logger.debug('Initializing boundary conditions')
275
+ for port in self.bc.oftype(LumpedPort):
276
+ self.define_lumped_port_integration_points(port)
277
+
278
+ def define_lumped_port_integration_points(self, port: LumpedPort) -> None:
279
+ """Sets the integration points on Lumped Port objects for voltage integration
280
+
281
+ Args:
282
+ port (LumpedPort): The LumpedPort object
283
+
284
+ Raises:
285
+ SimulationError: An error if there are no nodes associated with the port.
286
+ """
287
+ logger.debug('Finding Lumped Port integration points')
288
+ field_axis = port.Vdirection.np
289
+
290
+ points = self.mesh.get_nodes(port.tags)
291
+
292
+ if points.size==0:
293
+ raise SimulationError(f'The lumped port {port} has no nodes associated with it')
294
+ xs = self.mesh.nodes[0,points]
295
+ ys = self.mesh.nodes[1,points]
296
+ zs = self.mesh.nodes[2,points]
297
+
298
+ dotprod = xs*field_axis[0] + ys*field_axis[1] + zs*field_axis[2]
299
+
300
+ start_id = points[np.argwhere(dotprod == np.min(dotprod))]
301
+
302
+ start = np.squeeze(np.mean(self.mesh.nodes[:,start_id],axis=1))
303
+ logger.info(f'Starting node = {_dimstring(start)}')
304
+ end = start + port.Vdirection.np*port.height
305
+
306
+
307
+ port.vintline = Line.from_points(start, end, 21)
308
+
309
+ logger.info(f'Ending node = {_dimstring(end)}')
310
+ port.voltage_integration_points = (start, end)
311
+ port.v_integration = True
312
+
313
+ def _compute_integration_line(self, group1: list[int], group2: list[int]) -> tuple[np.ndarray, np.ndarray]:
314
+ """Computes an integration line for two node island groups by finding the closest two nodes.
315
+
316
+ This method is used for the modal TEM analysis to find an appropriate voltage integration path
317
+ by looking for the two closest points for the two conductor islands that where discovered.
318
+
319
+ Currently it defaults to 11 integration line points.
320
+
321
+ Args:
322
+ group1 (list[int]): The first island node group
323
+ group2 (list[int]): The second island node group
324
+
325
+ Returns:
326
+ centers (np.ndarray): The center points of the line segments
327
+ dls (np.ndarray): The delta-path vectors for each line segment.
328
+ """
329
+ nodes1 = self.mesh.nodes[:,group1]
330
+ nodes2 = self.mesh.nodes[:,group2]
331
+ path = shortest_path(nodes1, nodes2, 21)
332
+ centres = (path[:,1:] + path[:,:-1])/2
333
+ dls = path[:,1:] - path[:,:-1]
334
+ return centres, dls
335
+
336
+ def _find_tem_conductors(self, port: ModalPort, sigtri: np.ndarray) -> tuple[list[int], list[int]]:
337
+ ''' Returns two lists of global node indices corresponding to the TEM port conductors.
338
+
339
+ This method is invoked during modal analysis with TEM modes. It looks at all edges
340
+ exterior to the boundary face triangulation and finds two small subsets of nodes that
341
+ lie on different exterior boundaries of the boundary face.
342
+
343
+ Args:
344
+ port (ModalPort): The modal port object.
345
+
346
+ Returns:
347
+ list[int]: A list of node integers of island 1.
348
+ list[int]: A list of node integers of island 2.
349
+ '''
350
+
351
+ logger.debug('Finding PEC TEM conductors')
352
+ pecs: list[PEC] = self.bc.oftype(PEC)
353
+ mesh = self.mesh
354
+
355
+ # Process all PEC Boundary Conditions
356
+ pec_edges = []
357
+ for pec in pecs:
358
+ face_tags = pec.tags
359
+ tri_ids = mesh.get_triangles(face_tags)
360
+ edge_ids = list(mesh.tri_to_edge[:,tri_ids].flatten())
361
+ pec_edges.extend(edge_ids)
362
+
363
+ # Process conductivity
364
+ for itri in mesh.get_triangles(port.tags):
365
+ if sigtri[itri] > 1e6:
366
+ edge_ids = list(mesh.tri_to_edge[:,itri].flatten())
367
+ pec_edges.extend(edge_ids)
368
+
369
+ pec_edges = set(pec_edges)
370
+
371
+ tri_ids = mesh.get_triangles(port.tags)
372
+ edge_ids = list(mesh.tri_to_edge[:,tri_ids].flatten())
373
+
374
+ pec_port = np.array([i for i in list(pec_edges) if i in set(edge_ids)])
375
+
376
+ pec_islands = mesh.find_edge_groups(pec_port)
377
+
378
+ self.basis._pec_islands = pec_islands
379
+ logger.debug(f'Found {len(pec_islands)} PEC islands.')
380
+
381
+ if len(pec_islands) != 2:
382
+ raise ValueError(f'Found {len(pec_islands)} PEC islands. Expected 2.')
383
+
384
+ groups = []
385
+ for island in pec_islands:
386
+ group = set()
387
+ for edge in island:
388
+ group.add(mesh.edges[0,edge])
389
+ group.add(mesh.edges[1,edge])
390
+ groups.append(sorted(list(group)))
391
+
392
+ group1 = groups[0]
393
+ group2 = groups[1]
394
+
395
+ return group1, group2
396
+
397
+ def _compute_modes(self, freq: float):
398
+ """Compute the modal port modes for a given frequency. Used internally by the frequency domain study.
399
+
400
+ Args:
401
+ freq (float): The simulation frequency
402
+ """
403
+ for bc in self.bc.oftype(ModalPort):
404
+
405
+ # If there is a port mode (at least one) and the port does not have mixed materials. No new analysis is needed
406
+ if not bc.mixed_materials and bc.initialized:
407
+ continue
408
+
409
+ self.modal_analysis(bc, 1, False, bc.TEM, freq=freq)
410
+
411
+ def modal_analysis(self,
412
+ port: ModalPort,
413
+ nmodes: int = 6,
414
+ direct: bool = True,
415
+ TEM: bool = False,
416
+ target_kz = None,
417
+ target_neff = None,
418
+ freq: float = None) -> None:
419
+ ''' Execute a modal analysis on a given ModalPort boundary condition.
420
+
421
+ Parameters:
422
+ -----------
423
+ port : ModalPort
424
+ The port object to execute the analysis for.
425
+ direct : bool
426
+ Whether to use the direct solver (LAPACK) if True. Otherwise it uses the iterative
427
+ ARPACK solver. The ARPACK solver required an estimate for the propagation constant and is faster
428
+ for a large number of Degrees of Freedom.
429
+ TEM : bool = True
430
+ Whether to estimate the propagation constant assuming its a TEM transmisison line.
431
+ target_k0 : float
432
+ The expected propagation constant to find a mode for (direct = False).
433
+ target_neff : float
434
+ The expected effective mode index defined as kz/k0 (1.0 = free space, <1 = TE/TM, >1=slow wavees)
435
+ freq : float = None
436
+ The desired frequency at which the mode is solved. If None then it uses the lowest frequency of the provided range.
437
+ '''
438
+ T0 = time.time()
439
+ if self.bc._initialized is False:
440
+ raise SimulationError('Cannot run a modal analysis because no boundary conditions have been assigned.')
441
+
442
+ self._initialize_field()
443
+ self._initialize_bc_data()
444
+
445
+ logger.debug('Retreiving material properties.')
446
+ ertet = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
447
+ urtet = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
448
+ condtet = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
449
+
450
+ er = np.zeros((3,3,self.mesh.n_tris,), dtype=np.complex128)
451
+ ur = np.zeros((3,3,self.mesh.n_tris,), dtype=np.complex128)
452
+ cond = np.zeros((self.mesh.n_tris,), dtype=np.complex128)
453
+
454
+ for itri in range(self.mesh.n_tris):
455
+ itet = self.mesh.tri_to_tet[0,itri]
456
+ er[:,:,itri] = ertet[:,:,itet]
457
+ ur[:,:,itri] = urtet[:,:,itet]
458
+ cond[itri] = condtet[itet]
459
+
460
+ itri_port = self.mesh.get_triangles(port.tags)
461
+
462
+ ermean = np.mean(er[er>0].flatten()[itri_port])
463
+ urmean = np.mean(ur[ur>0].flatten()[itri_port])
464
+ ermax = np.max(er[:,:,itri_port].flatten())
465
+ urmax = np.max(ur[:,:,itri_port].flatten())
466
+
467
+ if freq is None:
468
+ freq = self.frequencies[0]
469
+
470
+
471
+ k0 = 2*np.pi*freq/299792458
472
+ kmax = k0*np.sqrt(ermax.real*urmax.real)
473
+
474
+ logger.info('Assembling BMA Matrices')
475
+
476
+ Amatrix, Bmatrix, solve_ids, nlf = self.assembler.assemble_bma_matrices(self.basis, er, ur, cond, k0, port, self.bc.boundary_conditions)
477
+
478
+ logger.debug(f'Total of {Amatrix.shape[0]} Degrees of freedom.')
479
+ logger.debug(f'Applied frequency: {freq/1e9:.2f}GHz')
480
+ logger.debug(f'K0 = {k0} rad/m')
481
+
482
+ F = -1
483
+
484
+ if target_neff is not None:
485
+ target_kz = k0*target_neff
486
+
487
+ if target_kz is None:
488
+ if TEM:
489
+ target_kz = ermean*urmean*1.1*k0
490
+ else:
491
+ target_kz = ermean*urmean*0.7*k0
492
+
493
+
494
+ logger.debug(f'Solving for {solve_ids.shape[0]} degrees of freedom.')
495
+
496
+ eigen_values, eigen_modes, report = self.solveroutine.eig_boundary(Amatrix, Bmatrix, solve_ids, nmodes, direct, target_kz, sign=-1)
497
+
498
+ logger.debug(f'Eigenvalues: {np.sqrt(F*eigen_values)} rad/m')
499
+
500
+ port._er = er
501
+ port._ur = ur
502
+
503
+ nmodes_found = eigen_values.shape[0]
504
+
505
+ for i in range(nmodes_found):
506
+
507
+ Emode = np.zeros((nlf.n_field,), dtype=np.complex128)
508
+ eigenmode = eigen_modes[:,i]
509
+ Emode[solve_ids] = np.squeeze(eigenmode)
510
+ Emode = Emode * np.exp(-1j*np.angle(np.max(Emode)))
511
+
512
+ beta = min(k0*np.sqrt(ermax*urmax), np.emath.sqrt(-eigen_values[i]))
513
+ #beta = np.emath.sqrt(eigen_values[i])
514
+ residuals = -1
515
+
516
+ portfE = nlf.interpolate_Ef(Emode)
517
+ portfH = nlf.interpolate_Hf(Emode, k0, ur, beta)
518
+
519
+ P = compute_avg_power_flux(nlf, Emode, k0, ur, beta)
520
+
521
+ mode = port.add_mode(Emode, portfE, portfH, beta, k0, residuals, TEM=TEM, freq=freq)
522
+ if mode is None:
523
+ continue
524
+
525
+ Efxy = Emode[:nlf.n_xy]
526
+ Efz = Emode[nlf.n_xy:]
527
+ Ez = np.max(np.abs(Efz))
528
+ Exy = np.max(np.abs(Efxy))
529
+
530
+ # Exy = np.max(np.max(Emode))
531
+ # Ez = 0
532
+ if Ez/Exy < 1e-3 and not TEM:
533
+ logger.debug('Low Ez/Et ratio detected, assuming TE mode')
534
+ mode.modetype = 'TE'
535
+ elif Ez/Exy > 1e-3 and not TEM:
536
+ logger.debug('High Ez/Et ratio detected, assuming TM mode')
537
+ mode.modetype = 'TM'
538
+ elif TEM:
539
+ G1, G2 = self._find_tem_conductors(port, sigtri=cond)
540
+ cs, dls = self._compute_integration_line(G1,G2)
541
+ mode.modetype='TEM'
542
+ Ex, Ey, Ez = portfE(cs[0,:], cs[1,:], cs[2,:])
543
+ voltage = np.sum(Ex*dls[0,:] + Ey*dls[1,:] + Ez*dls[2,:])
544
+ mode.Z0 = voltage**2/(2*P)
545
+ logger.debug(f'Port Z0 = {mode.Z0}')
546
+
547
+ mode.set_power(P*port._qmode(k0)**2)
548
+
549
+ port.sort_modes()
550
+
551
+ logger.info(f'Total of {port.nmodes} found')
552
+
553
+ T2 = time.time()
554
+ logger.info(f'Elapsed time = {(T2-T0):.2f} seconds.')
555
+ return None
556
+
557
+ def frequency_domain(self,
558
+ parallel: bool = False,
559
+ njobs: int = 2,
560
+ harddisc_threshold: int = None,
561
+ harddisc_path: str = 'EMergeSparse',
562
+ frequency_groups: int = -1,
563
+ multi_processing: bool = False,
564
+ automatic_modal_analysis: bool = True) -> MWData:
565
+ """Executes a frequency domain study
566
+
567
+ The study is distributed over "njobs" workers.
568
+ As optional parameter you may set a harddisc_threshold as integer. This determines the maximum
569
+ number of degrees of freedom before which the jobs will be cahced to the harddisk. The
570
+ path that will be used to cache the sparse matrices can be specified.
571
+ Additionally the term frequency_groups may be specified. This number will define in how
572
+ many groups the matrices will be pre-computed before they are send to workers. This can minimize
573
+ the total amound of RAM memory used. For example with 11 frequencies in gruops of 4, the following
574
+ frequency indices will be precomputed and then solved: [[1,2,3,4],[5,6,7,8],[9,10,11]]
575
+
576
+ Args:
577
+ njobs (int, optional): The number of jobs. Defaults to 2.
578
+ harddisc_threshold (int, optional): The number of DOF limit. Defaults to None.
579
+ harddisc_path (str, optional): The cached matrix path name. Defaults to 'EMergeSparse'.
580
+ frequency_groups (int, optional): The number of frequency points in a solve group. Defaults to -1.
581
+ automatic_modal_analysis (bool, optional): Automatically compute port modes. Defaults to False.
582
+ multi_processing (bool, optional): Whether to use multiprocessing instead of multi-threaded (slower on most machines).
583
+
584
+ Raises:
585
+ SimulationError: An error associated witha a problem during the simulation.
586
+
587
+ Returns:
588
+ MWSimData: The dataset.
589
+ """
590
+
591
+ self._simstart = time.time()
592
+ if self.bc._initialized is False:
593
+ raise SimulationError('Cannot run a modal analysis because no boundary conditions have been assigned.')
594
+
595
+ self._initialize_field()
596
+ self._initialize_bc_data()
597
+
598
+ er = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
599
+ ur = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
600
+ cond = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
601
+
602
+ ### Does this move
603
+ logger.debug('Initializing frequency domain sweep.')
604
+
605
+ #### Port settings
606
+ all_ports = self.bc.oftype(PortBC)
607
+
608
+ ##### FOR PORT SWEEP SET ALL ACTIVE TO FALSE. THIS SHOULD BE FIXED LATER
609
+ ### COMPUTE WHICH TETS ARE CONNECTED TO PORT INDICES
610
+
611
+ for port in all_ports:
612
+ port.active=False
613
+
614
+
615
+ logger.info(f'Pre-assembling matrices of {len(self.frequencies)} frequency points.')
616
+
617
+ # Thread-local storage for per-thread resources
618
+ thread_local = threading.local()
619
+
620
+ ## DEFINE SOLVE FUNCTIONS
621
+ def get_routine():
622
+ if not hasattr(thread_local, "routine"):
623
+ thread_local.routine = self.solveroutine.duplicate().configure('MT')
624
+ return thread_local.routine
625
+
626
+ def run_job(job: SimJob):
627
+ routine = get_routine()
628
+ for A, b, ids, reuse in job.iter_Ab():
629
+ solution, report = routine.solve(A, b, ids, reuse, id=job.id)
630
+ job.submit_solution(solution, report)
631
+ return job
632
+
633
+ def run_job_single(job: SimJob):
634
+ for A, b, ids, reuse in job.iter_Ab():
635
+ solution, report = self.solveroutine.solve(A, b, ids, reuse, id=job.id)
636
+ job.submit_solution(solution, report)
637
+ return job
638
+
639
+ ## GROUP FREQUENCIES
640
+ # Each frequency group will be pre-assembled before submitting them to the parallel pool
641
+ freq_groups = []
642
+ if frequency_groups == -1:
643
+ freq_groups=[self.frequencies,]
644
+ else:
645
+ n = frequency_groups
646
+ freq_groups = [self.frequencies[i:i+n] for i in range(0, len(self.frequencies), n)]
647
+
648
+ results: list[SimJob] = []
649
+
650
+ ## Single threaded
651
+ job_id = 1
652
+
653
+ self._compute_modes(sum(self.frequencies)/len(self.frequencies))
654
+
655
+ if not parallel:
656
+ # ITERATE OVER FREQUENCIES
657
+ freq_groups
658
+ for i_group, fgroup in enumerate(freq_groups):
659
+ logger.debug(f'Precomputing group {i_group}.')
660
+ jobs = []
661
+ ## Assemble jobs
662
+ for ifreq, freq in enumerate(fgroup):
663
+ logger.debug(f'Simulation frequency = {freq/1e9:.3f} GHz')
664
+ if automatic_modal_analysis:
665
+ self._compute_modes(freq)
666
+ job = self.assembler.assemble_freq_matrix(self.basis, er, ur, cond,
667
+ self.bc.boundary_conditions,
668
+ freq,
669
+ cache_matrices=self.cache_matrices)
670
+ job.store_limit = harddisc_threshold
671
+ job.relative_path = harddisc_path
672
+ job.id = job_id
673
+ job_id += 1
674
+ jobs.append(job)
675
+
676
+ logger.info(f'Starting single threaded solve of {len(jobs)} jobs.')
677
+ group_results = [run_job_single(job) for job in jobs]
678
+ results.extend(group_results)
679
+ elif not multi_processing:
680
+ # MULTI THREADED
681
+ with ThreadPoolExecutor(max_workers=njobs) as executor:
682
+ # ITERATE OVER FREQUENCIES
683
+ for i_group, fgroup in enumerate(freq_groups):
684
+ logger.debug(f'Precomputing group {i_group}.')
685
+ jobs = []
686
+ ## Assemble jobs
687
+ for freq in fgroup:
688
+ logger.debug(f'Simulation frequency = {freq/1e9:.3f} GHz')
689
+ if automatic_modal_analysis:
690
+ self._compute_modes(freq)
691
+ job = self.assembler.assemble_freq_matrix(self.basis, er, ur, cond,
692
+ self.bc.boundary_conditions,
693
+ freq,
694
+ cache_matrices=self.cache_matrices)
695
+ job.store_limit = harddisc_threshold
696
+ job.relative_path = harddisc_path
697
+ job.id = job_id
698
+ job_id += 1
699
+ jobs.append(job)
700
+
701
+ logger.info(f'Starting distributed solve of {len(jobs)} jobs with {njobs} threads.')
702
+ group_results = list(executor.map(run_job, jobs))
703
+ results.extend(group_results)
704
+ executor.shutdown()
705
+ else:
706
+ ### MULTI PROCESSING
707
+ # Check for if __name__=="__main__" Guard
708
+ if not called_from_main_function():
709
+ raise SimulationError(
710
+ "Multiprocess support must be launched from your "
711
+ "if __name__ == '__main__' guard in the top-level script."
712
+ )
713
+ # Start parallel pool
714
+ with mp.Pool(processes=njobs) as pool:
715
+ for i_group, fgroup in enumerate(freq_groups):
716
+ logger.debug(f'Precomputing group {i_group}.')
717
+ jobs = []
718
+ # Assemble jobs
719
+ for freq in fgroup:
720
+ logger.debug(f'Simulation frequency = {freq/1e9:.3f} GHz')
721
+ if automatic_modal_analysis:
722
+ self._compute_modes(freq)
723
+
724
+ job = self.assembler.assemble_freq_matrix(
725
+ self.basis, er, ur, cond,
726
+ self.bc.boundary_conditions,
727
+ freq,
728
+ cache_matrices=self.cache_matrices
729
+ )
730
+
731
+ job.store_limit = harddisc_threshold
732
+ job.relative_path = harddisc_path
733
+ job.id = job_id
734
+ job_id += 1
735
+ jobs.append(job)
736
+
737
+ logger.info(
738
+ f'Starting distributed solve of {len(jobs)} jobs '
739
+ f'with {njobs} processes in parallel'
740
+ )
741
+ # Distribute taks
742
+ group_results = pool.map(run_job_multi, jobs)
743
+ results.extend(group_results)
744
+
745
+ thread_local.__dict__.clear()
746
+ logger.info('Solving complete')
747
+
748
+ ### Compute S-parameters and return
749
+ self._post_process(results, er, ur, cond)
750
+ return self.data
751
+
752
+ def eigenmode(self, search_frequency: float,
753
+ nmodes: int = 6,
754
+ k0_limit: float = 1,
755
+ direct: bool = False,
756
+ deep_search: bool = False,
757
+ mode: Literal['LM','LR','SR','LI','SI']='LM') -> MWData:
758
+ """Executes an eigenmode study
759
+
760
+
761
+
762
+ Args:
763
+ search_frequency (float): The frequency around which you would like to search
764
+ nmodes (int, optional): The number of jobs. Defaults to 6.
765
+ k0_limit (float): The lowest k0 value before which a mode is considered part of the null space. Defaults to 1e-3
766
+ Raises:
767
+ SimulationError: An error associated witha a problem during the simulation.
768
+
769
+ Returns:
770
+ MWSimData: The dataset.
771
+ """
772
+
773
+ self._simstart = time.time()
774
+ if self.bc._initialized is False:
775
+ raise SimulationError('Cannot run a modal analysis because no boundary conditions have been assigned.')
776
+
777
+ self._initialize_field()
778
+ self._initialize_bc_data()
779
+
780
+ er = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
781
+ ur = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
782
+ cond = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
783
+
784
+ ### Does this move
785
+ logger.debug('Initializing frequency domain sweep.')
786
+
787
+ logger.info(f'Pre-assembling matrices of {len(self.frequencies)} frequency points.')
788
+
789
+ job = self.assembler.assemble_eig_matrix(self.basis, er, ur, cond,
790
+ self.bc.boundary_conditions, search_frequency)
791
+
792
+
793
+ logger.info('Solving complete')
794
+
795
+ A, C, solve_ids = job.yield_AC()
796
+
797
+ target_k0 = 2*np.pi*search_frequency/299792458
798
+
799
+ eigen_values, eigen_modes, report = self.solveroutine.eig(A, C, solve_ids, nmodes, direct, target_k0, which=mode)
800
+
801
+ eigen_modes = job.fix_solutions(eigen_modes)
802
+
803
+ logger.debug(f'Eigenvalues: {np.sqrt(eigen_values)} rad/m')
804
+
805
+ nmodes_found = eigen_values.shape[0]
806
+
807
+ for i in range(nmodes_found):
808
+
809
+ Emode = np.zeros((self.basis.n_field,), dtype=np.complex128)
810
+ eig_k0 = np.sqrt(eigen_values[i])
811
+ if eig_k0 < k0_limit:
812
+ logger.debug(f'Ignoring mode due to low k0: {eig_k0} < {k0_limit}')
813
+ continue
814
+ eig_freq = eig_k0*299792458/(2*np.pi)
815
+
816
+ logger.debug(f'Found k0={eig_k0:.2f}, f0={eig_freq/1e9:.2f} GHz')
817
+ Emode = eigen_modes[:,i]
818
+
819
+ scalardata = self.data.scalar.new(freq=eig_freq, **self._params)
820
+ scalardata.k0 = eig_k0
821
+ scalardata.freq = eig_freq
822
+
823
+ fielddata = self.data.field.new(freq=eig_freq, **self._params)
824
+ fielddata.freq = eig_freq
825
+ fielddata._der = np.squeeze(er[0,0,:])
826
+ fielddata._dur = np.squeeze(ur[0,0,:])
827
+ fielddata._mode_field = Emode
828
+ fielddata.basis = self.basis
829
+ ### Compute S-parameters and return
830
+
831
+ return self.data
832
+
833
+ def _post_process(self, results: list[SimJob], er: np.ndarray, ur: np.ndarray, cond: np.ndarray):
834
+ """Compute the S-parameters after Frequency sweep
835
+
836
+ Args:
837
+ results (list[SimJob]): The set of simulation results
838
+ er (np.ndarray): The domain εᵣ
839
+ ur (np.ndarray): The domain μᵣ
840
+ cond (np.ndarray): The domain conductivity
841
+ """
842
+ mesh = self.mesh
843
+ all_ports = self.bc.oftype(PortBC)
844
+ port_numbers = [port.port_number for port in all_ports]
845
+ all_port_tets = self.mesh.get_face_tets(*[port.tags for port in all_ports])
846
+
847
+ logger.info('Computing S-parameters')
848
+
849
+ ertri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
850
+ urtri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
851
+ condtri = np.zeros((self.mesh.n_tris,), dtype=np.complex128)
852
+
853
+ for itri in range(self.mesh.n_tris):
854
+ itet = self.mesh.tri_to_tet[0,itri]
855
+ ertri[:,:,itri] = er[:,:,itet]
856
+ urtri[:,:,itri] = ur[:,:,itet]
857
+ condtri[itri] = cond[itet]
858
+
859
+ for freq, job in zip(self.frequencies, results):
860
+
861
+ k0 = 2*np.pi*freq/299792458
862
+
863
+ scalardata = self.data.scalar.new(freq=freq, **self._params)
864
+ scalardata.k0 = k0
865
+ scalardata.freq = freq
866
+ scalardata.init_sp(port_numbers)
867
+
868
+ fielddata = self.data.field.new(freq=freq, **self._params)
869
+ fielddata.freq = freq
870
+ fielddata._der = np.squeeze(er[0,0,:])
871
+ fielddata._dur = np.squeeze(ur[0,0,:])
872
+
873
+ self.data.setreport(job.reports, freq=freq, **self._params)
874
+
875
+ logger.info(f'Post Processing simulation frequency = {freq/1e9:.3f} GHz')
876
+
877
+ # Recording port information
878
+ for active_port in all_ports:
879
+ fielddata.add_port_properties(active_port.port_number,
880
+ mode_number=active_port.mode_number,
881
+ k0 = k0,
882
+ beta = active_port.get_beta(k0),
883
+ Z0 = active_port.portZ0(k0),
884
+ Pout= active_port.power)
885
+ scalardata.add_port_properties(active_port.port_number,
886
+ mode_number=active_port.mode_number,
887
+ k0 = k0,
888
+ beta = active_port.get_beta(k0),
889
+ Z0 = active_port.portZ0(k0),
890
+ Pout= active_port.power)
891
+
892
+ # Set port as active and add the port mode to the forcing vector
893
+ active_port.active = True
894
+
895
+ solution = job._fields[active_port.port_number]
896
+
897
+ fielddata._fields = job._fields
898
+ fielddata.basis = self.basis
899
+ # Compute the S-parameters
900
+ # Define the field interpolation function
901
+ fieldf = self.basis.interpolate_Ef(solution, tetids=all_port_tets)
902
+ Pout = 0
903
+
904
+ # Active port power
905
+ logger.debug('Active ports:')
906
+ tris = mesh.get_triangles(active_port.tags)
907
+ tri_vertices = mesh.tris[:,tris]
908
+ pfield, pmode = self._compute_s_data(active_port, fieldf, tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
909
+ logger.debug(f' Field Amplitude = {np.abs(pfield):.3f}, Excitation = {np.abs(pmode):.2f}')
910
+ Pout = pmode
911
+
912
+ #Passive ports
913
+ logger.debug('Passive ports:')
914
+ for bc in all_ports:
915
+ tris = mesh.get_triangles(bc.tags)
916
+ tri_vertices = mesh.tris[:,tris]
917
+ pfield, pmode = self._compute_s_data(bc, fieldf,tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
918
+ logger.debug(f' Field amplitude = {np.abs(pfield):.3f}, Excitation= {np.abs(pmode):.2f}')
919
+ scalardata.write_S(bc.port_number, active_port.port_number, pfield/Pout)
920
+ active_port.active=False
921
+
922
+ fielddata.set_field_vector()
923
+
924
+ logger.info('Simulation Complete!')
925
+ self._simend = time.time()
926
+ logger.info(f'Elapsed time = {(self._simend-self._simstart):.2f} seconds.')
927
+
928
+
929
+ def _compute_s_data(self, bc: PortBC,
930
+ fieldfunction: Callable,
931
+ tri_vertices: np.ndarray,
932
+ k0: float,
933
+ erp: np.ndarray,
934
+ urp: np.ndarray,) -> tuple[complex, complex]:
935
+ """ Computes the S-parameter data for a given boundary condition and field function.
936
+
937
+ Args:
938
+ bc (PortBC): The port boundary condition
939
+ fieldfunction (Callable): The field function that interpolates the solution field.
940
+ tri_vertices (np.ndarray): The triangle vertex indices of the port face
941
+ k₀ (float): The simulation phase constant
942
+ erp (np.ndarray): The εᵣ of the port face triangles
943
+ urp (np.ndarray): The μᵣ of the port face triangles.
944
+
945
+ Returns:
946
+ tuple[complex, complex]: _description_
947
+ """
948
+ from .sparam import sparam_field_power, sparam_mode_power
949
+ if bc.v_integration:
950
+ V = bc.vintline.line_integral(fieldfunction)
951
+
952
+ if bc.active:
953
+ a = bc.voltage
954
+ b = (V-bc.voltage)
955
+ else:
956
+ a = 0
957
+ b = V
958
+
959
+ a = a*csqrt(1/(2*bc.Z0))
960
+ b = b*csqrt(1/(2*bc.Z0))
961
+
962
+ return b, a
963
+ else:
964
+ if bc.modetype(k0) == 'TEM':
965
+ const = 1/(np.sqrt((urp[0,0,:] + urp[1,1,:] + urp[2,2,:])/(erp[0,0,:] + erp[1,1,:] + erp[2,2,:])))
966
+ if bc.modetype(k0) == 'TE':
967
+ const = 1/((urp[0,0,:] + urp[1,1,:] + urp[2,2,:])/3)
968
+ elif bc.modetype(k0) == 'TM':
969
+ const = 1/((erp[0,0,:] + erp[1,1,:] + erp[2,2,:])/3)
970
+ const = np.squeeze(const)
971
+ field_p = sparam_field_power(self.mesh.nodes, tri_vertices, bc, k0, fieldfunction, const)
972
+ mode_p = sparam_mode_power(self.mesh.nodes, tri_vertices, bc, k0, const)
973
+ return field_p, mode_p
974
+
975
+ # def frequency_domain_single(self, automatic_modal_analysis: bool = False) -> MWData:
976
+ # """Execute a frequency domain study without distributed frequency sweep.
977
+
978
+ # Args:
979
+ # automatic_modal_analysis (bool, optional): Automatically compute port modes. Defaults to False.
980
+
981
+ # Raises:
982
+ # SimulationError: _description_
983
+
984
+ # Returns:
985
+ # MWSimData: The Simulation data.
986
+ # """
987
+ # T0 = time.time()
988
+ # mesh = self.mesh
989
+ # if self.bc._initialized is False:
990
+ # raise SimulationError('Cannot run a modal analysis because no boundary conditions have been assigned.')
991
+
992
+ # self._initialize_field()
993
+ # self._initialize_bc_data()
994
+
995
+ # er = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
996
+ # ur = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
997
+ # cond = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
998
+
999
+ # ertri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
1000
+ # urtri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
1001
+ # condtri = np.zeros((self.mesh.n_tris,), dtype=np.complex128)
1002
+
1003
+ # for itri in range(self.mesh.n_tris):
1004
+ # itet = self.mesh.tri_to_tet[0,itri]
1005
+ # ertri[:,:,itri] = er[:,:,itet]
1006
+ # urtri[:,:,itri] = ur[:,:,itet]
1007
+ # condtri[itri] = cond[itet]
1008
+
1009
+ # #### Port settings
1010
+
1011
+ # all_ports = self.bc.oftype(PortBC)
1012
+ # port_numbers = [port.port_number for port in all_ports]
1013
+
1014
+ # ##### FOR PORT SWEEP SET ALL ACTIVE TO FALSE. THIS SHOULD BE FIXED LATER
1015
+ # ### COMPUTE WHICH TETS ARE CONNECTED TO PORT INDICES
1016
+
1017
+ # all_port_tets = []
1018
+ # for port in all_ports:
1019
+ # port.active=False
1020
+
1021
+ # all_port_tets = mesh.get_face_tets(*[port.tags for port in all_ports])
1022
+
1023
+
1024
+ # logger.debug(f'Starting the simulation of {len(self.frequencies)} frequency points.')
1025
+
1026
+ # # ITERATE OVER FREQUENCIES
1027
+ # for freq in self.frequencies:
1028
+ # logger.info(f'Simulation frequency = {freq/1e9:.3f} GHz')
1029
+
1030
+ # # Assembling matrix problem
1031
+ # if automatic_modal_analysis:
1032
+ # self._compute_modes(freq)
1033
+
1034
+ # job = self.assembler.assemble_freq_matrix(self.basis, er, ur, cond, self.bc.boundary_conditions, freq, cache_matrices=self.cache_matrices)
1035
+
1036
+ # logger.debug(f'Routine: {self.solveroutine}')
1037
+
1038
+ # for A, b, ids, reuse in job.iter_Ab():
1039
+ # solution, report = self.solveroutine.solve(A, b, ids, reuse)
1040
+ # job.submit_solution(solution, report)
1041
+
1042
+ # self.data.setreport(job.reports, freq=freq, **self._params)
1043
+
1044
+ # k0 = 2*np.pi*freq/299792458
1045
+
1046
+ # scalardata = self.data.scalar.new(freq=freq, **self._params)
1047
+ # scalardata.init_sp(port_numbers)
1048
+ # scalardata.freq = freq
1049
+ # scalardata.k0 = k0
1050
+
1051
+ # fielddata = self.data.field.new(freq=freq, **self._params)
1052
+ # fielddata.freq = freq
1053
+ # fielddata.er = np.squeeze(er[0,0,:])
1054
+ # fielddata.ur = np.squeeze(ur[0,0,:])
1055
+
1056
+ # # Recording port information
1057
+ # for i, port in enumerate(all_ports):
1058
+ # fielddata.add_port_properties(port.port_number,
1059
+ # mode_number=port.mode_number,
1060
+ # k0 = k0,
1061
+ # beta = port.get_beta(k0),
1062
+ # Z0 = port.portZ0(k0),
1063
+ # Pout= port.power)
1064
+
1065
+ # for active_port in all_ports:
1066
+
1067
+ # active_port.active = True
1068
+ # solution = job._fields[active_port.port_number]
1069
+
1070
+ # fielddata._fields[active_port.port_number] = solution # TODO: THIS IS VERY FRAIL
1071
+ # fielddata.basis = self.basis
1072
+
1073
+ # # Compute the S-parameters
1074
+ # # Define the field interpolation function
1075
+ # fieldf = self.basis.interpolate_Ef(solution, tetids=all_port_tets)
1076
+
1077
+ # # Active port power
1078
+ # logger.debug('Active ports:')
1079
+ # tris = mesh.get_triangles(active_port.tags)
1080
+ # tri_vertices = mesh.tris[:,tris]
1081
+ # pfield, pmode = self._compute_s_data(active_port, fieldf, tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
1082
+ # logger.debug(f' Field Amplitude = {np.abs(pfield):.3f}, Excitation = {np.abs(pmode):.2f}')
1083
+ # Pout = pmode
1084
+
1085
+ # #Passive ports
1086
+ # logger.debug('Passive ports:')
1087
+ # for bc in all_ports:
1088
+ # tris = mesh.get_triangles(bc.tags)
1089
+ # tri_vertices = mesh.tris[:,tris]
1090
+ # pfield, pmode = self._compute_s_data(bc, fieldf, tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
1091
+ # logger.debug(f' Field amplitude = {np.abs(pfield):.3f}, Excitation= {np.abs(pmode):.2f}')
1092
+ # scalardata.write_S(bc.port_number, active_port.port_number, pfield/Pout)
1093
+
1094
+ # active_port.active=False
1095
+
1096
+ # fielddata.set_field_vector()
1097
+
1098
+ # logger.info('Simulation Complete!')
1099
+ # T2 = time.time()
1100
+ # logger.info(f'Elapsed time = {(T2-T0):.2f} seconds.')
1101
+ # return self.data
1102
+ ## DEPRICATED
1103
+
1104
+
1105
+
1106
+ #
1107
+ # def eigenmode(self, mesh: Mesh3D, solver = None, num_sols: int = 6):
1108
+ # if solver is None:
1109
+ # logger.info('Defaulting to BiCGStab.')
1110
+ # solver = sparse.linalg.eigs
1111
+
1112
+ # if self.order == 1:
1113
+ # logger.info('Detected 1st order elements.')
1114
+ # from ...elements.nedelec1.assembly import assemble_eig_matrix
1115
+ # ft = FieldType.VEC_LIN
1116
+
1117
+ # elif self.order == 2:
1118
+ # logger.info('Detected 2nd order elements.')
1119
+ # from ...elements.nedelec2.assembly import assemble_eig_matrix_E
1120
+ # ft = FieldType.VEC_QUAD
1121
+
1122
+ # er = self.mesh.retreive(mesh.centers, lambda mat,x,y,z: mat.fer3d(x,y,z))
1123
+ # ur = self.mesh.retreive(mesh.centers, lambda mat,x,y,z: mat.fur3d(x,y,z))
1124
+
1125
+ # dataset = Dataset3D(mesh, self.frequencies, 0, ft)
1126
+ # dataset.er = er
1127
+ # dataset.ur = ur
1128
+ # logger.info('Solving eigenmodes.')
1129
+
1130
+ # f_target = self.frequencies[0]
1131
+ # sigma = (2 * np.pi * f_target / 299792458)**2
1132
+
1133
+ # A, B, solvenodes = assemble_eig_matrix(mesh, er, ur, self.boundary_conditions)
1134
+
1135
+ # A = A[np.ix_(solvenodes, solvenodes)]
1136
+ # B = B[np.ix_(solvenodes, solvenodes)]
1137
+ # #A = sparse.csc_matrix(A)
1138
+ # #B = sparse.csc_matrix(B)
1139
+
1140
+ # w, v = sparse.linalg.eigs(A, k=num_sols, M=B, sigma=sigma, which='LM')
1141
+
1142
+ # logger.info(f'Eigenvalues: {np.sqrt(w)*299792458/(2*np.pi) * 1e-9} GHz')
1143
+
1144
+ # Esol = np.zeros((num_sols, mesh.nfield), dtype=np.complex128)
1145
+
1146
+ # Esol[:, solvenodes] = v.T
1147
+
1148
+ # dataset.set_efield(Esol)
1149
+
1150
+ # self.basis = dataset