emerge 1.0.2__py3-none-any.whl → 1.0.4__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 +7 -3
- emerge/_emerge/elements/femdata.py +5 -1
- emerge/_emerge/elements/ned2_interp.py +73 -30
- emerge/_emerge/elements/nedelec2.py +1 -0
- emerge/_emerge/emerge_update.py +63 -0
- emerge/_emerge/geo/operations.py +2 -1
- emerge/_emerge/geo/polybased.py +26 -5
- emerge/_emerge/geometry.py +5 -0
- emerge/_emerge/logsettings.py +26 -1
- emerge/_emerge/material.py +29 -8
- emerge/_emerge/mesh3d.py +16 -13
- emerge/_emerge/mesher.py +70 -3
- emerge/_emerge/physics/microwave/assembly/assembler.py +5 -4
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +0 -1
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +1 -2
- emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +1 -1
- emerge/_emerge/physics/microwave/assembly/robin_abc_order2.py +0 -1
- emerge/_emerge/physics/microwave/microwave_3d.py +37 -16
- emerge/_emerge/physics/microwave/microwave_bc.py +15 -4
- emerge/_emerge/physics/microwave/microwave_data.py +14 -11
- emerge/_emerge/plot/pyvista/cmap_maker.py +70 -0
- emerge/_emerge/plot/pyvista/display.py +101 -37
- emerge/_emerge/simmodel.py +75 -21
- emerge/_emerge/simulation_data.py +22 -4
- emerge/_emerge/solver.py +78 -51
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/METADATA +2 -3
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/RECORD +30 -28
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/WHEEL +0 -0
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/licenses/LICENSE +0 -0
emerge/_emerge/mesher.py
CHANGED
|
@@ -210,9 +210,41 @@ class Mesher:
|
|
|
210
210
|
gmsh.model.mesh.field.set_numbers(ctag, "SurfacesList", tags)
|
|
211
211
|
gmsh.model.mesh.field.set_number(ctag, "VIn", max_size)
|
|
212
212
|
self.mesh_fields.append(ctag)
|
|
213
|
+
|
|
214
|
+
def _set_size_on_edge(self, tags: list[int], max_size: float) -> None:
|
|
215
|
+
"""Define the size of the mesh on an edge
|
|
213
216
|
|
|
214
|
-
|
|
217
|
+
Args:
|
|
218
|
+
tags (list[int]): The tags of the geometry
|
|
219
|
+
max_size (float): The maximum size (in meters)
|
|
220
|
+
"""
|
|
221
|
+
ctag = gmsh.model.mesh.field.add("Constant")
|
|
222
|
+
gmsh.model.mesh.field.set_numbers(ctag, "CurvesList", tags)
|
|
223
|
+
gmsh.model.mesh.field.set_number(ctag, "VIn", max_size)
|
|
224
|
+
self.mesh_fields.append(ctag)
|
|
215
225
|
|
|
226
|
+
def _set_size_on_point(self, tags: list[int], max_size: float) -> None:
|
|
227
|
+
"""Define the size of the mesh on a point
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
tags (list[int]): The tags of the geometry
|
|
231
|
+
max_size (float): The maximum size (in meters)
|
|
232
|
+
"""
|
|
233
|
+
ctag = gmsh.model.mesh.field.add("Constant")
|
|
234
|
+
gmsh.model.mesh.field.set_numbers(ctag, "PointsList", tags)
|
|
235
|
+
gmsh.model.mesh.field.set_number(ctag, "VIn", max_size)
|
|
236
|
+
self.mesh_fields.append(ctag)
|
|
237
|
+
|
|
238
|
+
def _configure_mesh_size(self, discretizer: Callable, resolution: float):
|
|
239
|
+
"""Defines the mesh sizes based on a discretization callable.
|
|
240
|
+
The discretizer must take a material and return a maximum
|
|
241
|
+
size for that material.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
discretizer (Callable): The discretization function
|
|
245
|
+
resolution (float): The resolution
|
|
246
|
+
"""
|
|
247
|
+
logger.debug('Starting initial mesh size computation.')
|
|
216
248
|
dimtags = gmsh.model.occ.get_entities(2)
|
|
217
249
|
|
|
218
250
|
for dim, tag in dimtags:
|
|
@@ -227,21 +259,24 @@ class Mesher:
|
|
|
227
259
|
|
|
228
260
|
size = discretizer(obj.material)*resolution*obj.mesh_multiplier
|
|
229
261
|
size = min(size, obj.max_meshsize)
|
|
230
|
-
|
|
262
|
+
|
|
231
263
|
for tag in obj.tags:
|
|
232
264
|
size_mapping[tag] = size
|
|
233
265
|
|
|
234
266
|
for tag, size in size_mapping.items():
|
|
267
|
+
logger.debug(f'Setting mesh size:{1000*size:.3f}mm in domains: {tag}')
|
|
235
268
|
self._set_size_in_domain([tag,], size)
|
|
236
269
|
|
|
237
270
|
gmsh.model.mesh.field.setNumbers(mintag, "FieldsList", self.mesh_fields)
|
|
238
271
|
gmsh.model.mesh.field.setAsBackgroundMesh(mintag)
|
|
239
272
|
|
|
240
273
|
for tag, size in self.size_definitions:
|
|
274
|
+
logger.debug(f'Setting aux size definition: {1000*size:.3f}mm in domain {tag}.')
|
|
241
275
|
gmsh.model.mesh.setSize([tag,], size)
|
|
242
276
|
|
|
243
277
|
def unset_constraints(self, dimtags: list[tuple[int,int]]):
|
|
244
278
|
'''Unset the mesh constraints for the given dimension tags.'''
|
|
279
|
+
logger.trace(f'Unsetting mesh size constraint for domains: {dimtags}')
|
|
245
280
|
for dimtag in dimtags:
|
|
246
281
|
gmsh.model.mesh.setSizeFromBoundary(dimtag[0], dimtag[1], 0)
|
|
247
282
|
|
|
@@ -263,13 +298,16 @@ class Mesher:
|
|
|
263
298
|
return
|
|
264
299
|
|
|
265
300
|
dimtags = boundary.dimtags
|
|
266
|
-
|
|
301
|
+
|
|
302
|
+
|
|
267
303
|
if max_size is None:
|
|
268
304
|
self._check_ready()
|
|
269
305
|
max_size = self.max_size
|
|
270
306
|
|
|
271
307
|
growth_distance = np.log10(max_size/size)/np.log10(growth_rate)
|
|
272
308
|
|
|
309
|
+
logger.debug(f'Setting boundary size for region {dimtags} to {size*1000:.3f}mm, GR={growth_rate:.3f}, dist={growth_distance*1000:.2f}mm, Max={max_size*1000:.3f}mm')
|
|
310
|
+
|
|
273
311
|
nodes = gmsh.model.getBoundary(dimtags, combined=False, oriented=False, recursive=False)
|
|
274
312
|
|
|
275
313
|
disttag = gmsh.model.mesh.field.add("Distance")
|
|
@@ -295,6 +333,12 @@ class Mesher:
|
|
|
295
333
|
obj (GeoVolume | Selection): The volumetric domain
|
|
296
334
|
size (float): The maximum mesh size
|
|
297
335
|
"""
|
|
336
|
+
if obj.dim != 3:
|
|
337
|
+
logger.warning('Provided object is not a volume.')
|
|
338
|
+
if obj.dim==2:
|
|
339
|
+
logger.warning('Forwarding to set_face_size')
|
|
340
|
+
self.set_face_size(obj, size)
|
|
341
|
+
logger.debug(f'Setting size {size*1000:.3f}ff for object {obj}')
|
|
298
342
|
self._set_size_in_domain(obj.tags, size)
|
|
299
343
|
|
|
300
344
|
def set_face_size(self, obj: GeoSurface | Selection, size: float):
|
|
@@ -304,7 +348,30 @@ class Mesher:
|
|
|
304
348
|
obj (GeoSurface | Selection): The surface domain
|
|
305
349
|
size (float): The maximum size
|
|
306
350
|
"""
|
|
351
|
+
if obj.dim != 2:
|
|
352
|
+
logger.warning('Provided object is not a surface.')
|
|
353
|
+
if obj.dim==3:
|
|
354
|
+
logger.warning('Forwarding to set_domain_size')
|
|
355
|
+
self.set_face_size(obj, size)
|
|
356
|
+
|
|
357
|
+
logger.debug(f'Setting size {size*1000:.3f}ff for face {obj}')
|
|
307
358
|
self._set_size_on_face(obj.tags, size)
|
|
359
|
+
|
|
360
|
+
def set_size(self, obj: GeoObject, size: float) -> None:
|
|
361
|
+
"""Manually set the size in or on an object
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
obj (GeoObject): _description_
|
|
365
|
+
size (float): _description_
|
|
366
|
+
"""
|
|
367
|
+
if obj.dim == 2:
|
|
368
|
+
self._set_size_on_face(obj.tags, size)
|
|
369
|
+
elif obj.dim == 3:
|
|
370
|
+
self._set_size_in_domain(obj.tags, size)
|
|
371
|
+
elif obj.dim == 1:
|
|
372
|
+
self._set_size_on_edge(obj.tags, size)
|
|
373
|
+
elif obj.dim == 0:
|
|
374
|
+
self._set_size_on_point(obj.tags, size)
|
|
308
375
|
|
|
309
376
|
def refine_conductor_edge(self, dimtags: list[tuple[int,int]], size):
|
|
310
377
|
nodes = gmsh.model.getBoundary(dimtags, combined=False, recursive=False)
|
|
@@ -27,6 +27,7 @@ from ..simjob import SimJob
|
|
|
27
27
|
|
|
28
28
|
from ....const import MU0, EPS0, C0
|
|
29
29
|
|
|
30
|
+
_PBC_DSMAX = 1e-15
|
|
30
31
|
|
|
31
32
|
############################################################
|
|
32
33
|
# FUNCTIONS #
|
|
@@ -361,8 +362,8 @@ class Assembler:
|
|
|
361
362
|
edge_ids_2 = mesh.get_edges(pbc.face2.tags)
|
|
362
363
|
dv = np.array(pbc.dv)
|
|
363
364
|
logger.trace(f'..displacement vector {dv}')
|
|
364
|
-
linked_tris = pair_coordinates(mesh.tri_centers, tri_ids_1, tri_ids_2, dv,
|
|
365
|
-
linked_edges = pair_coordinates(mesh.edge_centers, edge_ids_1, edge_ids_2, dv,
|
|
365
|
+
linked_tris = pair_coordinates(mesh.tri_centers, tri_ids_1, tri_ids_2, dv, _PBC_DSMAX)
|
|
366
|
+
linked_edges = pair_coordinates(mesh.edge_centers, edge_ids_1, edge_ids_2, dv, _PBC_DSMAX)
|
|
366
367
|
dv = np.array(pbc.dv)
|
|
367
368
|
phi = pbc.phi(K0)
|
|
368
369
|
logger.trace(f'..ϕ={phi} rad/m')
|
|
@@ -547,8 +548,8 @@ class Assembler:
|
|
|
547
548
|
tri_ids_2 = mesh.get_triangles(bcp.face2.tags)
|
|
548
549
|
edge_ids_2 = mesh.get_edges(bcp.face2.tags)
|
|
549
550
|
dv = np.array(bcp.dv)
|
|
550
|
-
linked_tris = pair_coordinates(mesh.tri_centers, tri_ids_1, tri_ids_2, dv,
|
|
551
|
-
linked_edges = pair_coordinates(mesh.edge_centers, edge_ids_1, edge_ids_2, dv,
|
|
551
|
+
linked_tris = pair_coordinates(mesh.tri_centers, tri_ids_1, tri_ids_2, dv, _PBC_DSMAX)
|
|
552
|
+
linked_edges = pair_coordinates(mesh.edge_centers, edge_ids_1, edge_ids_2, dv, _PBC_DSMAX)
|
|
552
553
|
dv = np.array(bcp.dv)
|
|
553
554
|
phi = bcp.phi(k0)
|
|
554
555
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import numpy as np
|
|
19
19
|
from ....elements.nedleg2 import NedelecLegrange2
|
|
20
20
|
from scipy.sparse import csr_matrix
|
|
21
|
-
from ....mth.optimized import local_mapping, matinv, compute_distances
|
|
21
|
+
from ....mth.optimized import local_mapping, matinv, compute_distances
|
|
22
22
|
from numba import c16, types, f8, i8, njit, prange
|
|
23
23
|
|
|
24
24
|
|
|
@@ -48,7 +48,6 @@ def generelized_eigenvalue_matrix(field: NedelecLegrange2,
|
|
|
48
48
|
edges = field.mesh.edges
|
|
49
49
|
nodes = field.mesh.nodes
|
|
50
50
|
|
|
51
|
-
nT = tris.shape[1]
|
|
52
51
|
tri_to_field = field.tri_to_field
|
|
53
52
|
|
|
54
53
|
nodes = field.local_nodes
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import numpy as np
|
|
19
19
|
from ....elements.nedleg2 import NedelecLegrange2
|
|
20
20
|
from scipy.sparse import csr_matrix
|
|
21
|
-
from ....mth.optimized import local_mapping, matinv, compute_distances
|
|
21
|
+
from ....mth.optimized import local_mapping, matinv, compute_distances
|
|
22
22
|
from numba import c16, types, f8, i8, njit, prange
|
|
23
23
|
|
|
24
24
|
|
|
@@ -262,6 +262,7 @@ class Microwave3D:
|
|
|
262
262
|
logger.warning('A resolution greater than 0.33 may cause accuracy issues.')
|
|
263
263
|
|
|
264
264
|
self.resolution = resolution
|
|
265
|
+
logger.trace(f'Resolution set to {self.resolution}')
|
|
265
266
|
|
|
266
267
|
def set_conductivity_limit(self, condutivity: float) -> None:
|
|
267
268
|
"""Sets the limit of a material conductivity value beyond which
|
|
@@ -274,7 +275,9 @@ class Microwave3D:
|
|
|
274
275
|
if condutivity < 0:
|
|
275
276
|
raise ValueError('Conductivity values must be above 0. Ignoring assignment')
|
|
276
277
|
|
|
277
|
-
self.assembler.
|
|
278
|
+
self.assembler.settings.mw_2dbc_peclim = condutivity
|
|
279
|
+
self.assembler.settings.mw_3d_peclim = condutivity
|
|
280
|
+
logger.trace(f'Set conductivity limit to {condutivity} S/m')
|
|
278
281
|
|
|
279
282
|
def get_discretizer(self) -> Callable:
|
|
280
283
|
"""Returns a discretizer function that defines the maximum mesh size.
|
|
@@ -295,7 +298,7 @@ class Microwave3D:
|
|
|
295
298
|
if self.basis is not None:
|
|
296
299
|
return
|
|
297
300
|
if self.order == 1:
|
|
298
|
-
raise NotImplementedError('Nedelec 1 is
|
|
301
|
+
raise NotImplementedError('Nedelec 1 is currently not supported')
|
|
299
302
|
from ...elements import Nedelec1
|
|
300
303
|
self.basis = Nedelec1(self.mesh)
|
|
301
304
|
elif self.order == 2:
|
|
@@ -310,6 +313,11 @@ class Microwave3D:
|
|
|
310
313
|
for port in self.bc.oftype(LumpedPort):
|
|
311
314
|
self.define_lumped_port_integration_points(port)
|
|
312
315
|
|
|
316
|
+
def _check_physics(self) -> None:
|
|
317
|
+
""" Executes a physics check before a simulation can be run."""
|
|
318
|
+
if not self.bc._is_excited():
|
|
319
|
+
raise SimulationError('The simulation has no boundary conditions that insert energy. Make sure to include at least one Port into your simulation.')
|
|
320
|
+
|
|
313
321
|
def define_lumped_port_integration_points(self, port: LumpedPort) -> None:
|
|
314
322
|
"""Sets the integration points on Lumped Port objects for voltage integration
|
|
315
323
|
|
|
@@ -319,7 +327,9 @@ class Microwave3D:
|
|
|
319
327
|
Raises:
|
|
320
328
|
SimulationError: An error if there are no nodes associated with the port.
|
|
321
329
|
"""
|
|
322
|
-
|
|
330
|
+
if len(port.vintline) > 0:
|
|
331
|
+
return
|
|
332
|
+
logger.debug(' - Finding Lumped Port integration points')
|
|
323
333
|
field_axis = port.Vdirection.np
|
|
324
334
|
|
|
325
335
|
points = self.mesh.get_nodes(port.tags)
|
|
@@ -344,7 +354,7 @@ class Microwave3D:
|
|
|
344
354
|
start = np.array([x,y,z])
|
|
345
355
|
end = start + port.Vdirection.np*port.height
|
|
346
356
|
port.vintline.append(Line.from_points(start, end, 21))
|
|
347
|
-
logger.trace(f'Port[{port.port_number}] integration line {start} -> {end}.')
|
|
357
|
+
logger.trace(f' - Port[{port.port_number}] integration line {start} -> {end}.')
|
|
348
358
|
|
|
349
359
|
port.v_integration = True
|
|
350
360
|
|
|
@@ -388,7 +398,7 @@ class Microwave3D:
|
|
|
388
398
|
if self.basis is None:
|
|
389
399
|
raise ValueError('The field basis is not yet defined.')
|
|
390
400
|
|
|
391
|
-
logger.debug('Finding PEC TEM conductors')
|
|
401
|
+
logger.debug(' - Finding PEC TEM conductors')
|
|
392
402
|
pecs: list[PEC] = self.bc.get_conductors() # type: ignore
|
|
393
403
|
mesh = self.mesh
|
|
394
404
|
|
|
@@ -415,10 +425,10 @@ class Microwave3D:
|
|
|
415
425
|
|
|
416
426
|
pec_islands = mesh.find_edge_groups(pec_port)
|
|
417
427
|
|
|
418
|
-
logger.debug(f'Found {len(pec_islands)} PEC islands.')
|
|
428
|
+
logger.debug(f' - Found {len(pec_islands)} PEC islands.')
|
|
419
429
|
|
|
420
430
|
if len(pec_islands) != 2:
|
|
421
|
-
raise ValueError(f'Found {len(pec_islands)} PEC islands. Expected 2.')
|
|
431
|
+
raise ValueError(f' - Found {len(pec_islands)} PEC islands. Expected 2.')
|
|
422
432
|
|
|
423
433
|
groups = []
|
|
424
434
|
for island in pec_islands:
|
|
@@ -475,6 +485,8 @@ class Microwave3D:
|
|
|
475
485
|
The desired frequency at which the mode is solved. If None then it uses the lowest frequency of the provided range.
|
|
476
486
|
'''
|
|
477
487
|
T0 = time.time()
|
|
488
|
+
logger.info(f'Starting Mode Analysis for port {port}.')
|
|
489
|
+
|
|
478
490
|
if self.bc._initialized is False:
|
|
479
491
|
raise SimulationError('Cannot run a modal analysis because no boundary conditions have been assigned.')
|
|
480
492
|
|
|
@@ -484,7 +496,7 @@ class Microwave3D:
|
|
|
484
496
|
if self.basis is None:
|
|
485
497
|
raise SimulationError('Cannot proceed, the current basis class is undefined.')
|
|
486
498
|
|
|
487
|
-
logger.debug('
|
|
499
|
+
logger.debug(' - retreiving material properties.')
|
|
488
500
|
|
|
489
501
|
if freq is None:
|
|
490
502
|
freq = self.frequencies[0]
|
|
@@ -523,6 +535,8 @@ class Microwave3D:
|
|
|
523
535
|
|
|
524
536
|
k0 = 2*np.pi*freq/299792458
|
|
525
537
|
|
|
538
|
+
logger.debug(f' - mean(max): εr = {ermean:.2f}({ermax:.2f}), μr = {urmean:.2f}({urmax:.2f})')
|
|
539
|
+
|
|
526
540
|
Amatrix, Bmatrix, solve_ids, nlf = self.assembler.assemble_bma_matrices(self.basis, er, ur, cond, k0, port, self.bc)
|
|
527
541
|
|
|
528
542
|
logger.debug(f'Total of {Amatrix.shape[0]} Degrees of freedom.')
|
|
@@ -643,6 +657,7 @@ class Microwave3D:
|
|
|
643
657
|
|
|
644
658
|
self._initialize_field()
|
|
645
659
|
self._initialize_bc_data()
|
|
660
|
+
self._check_physics()
|
|
646
661
|
|
|
647
662
|
if self.basis is None:
|
|
648
663
|
raise SimulationError('Cannot proceed, the simulation basis class is undefined.')
|
|
@@ -664,8 +679,10 @@ class Microwave3D:
|
|
|
664
679
|
|
|
665
680
|
logger.info(f'Pre-assembling matrices of {len(self.frequencies)} frequency points.')
|
|
666
681
|
|
|
667
|
-
|
|
668
|
-
|
|
682
|
+
thread_local = None
|
|
683
|
+
if parallel:
|
|
684
|
+
# Thread-local storage for per-thread resources
|
|
685
|
+
thread_local = threading.local()
|
|
669
686
|
|
|
670
687
|
## DEFINE SOLVE FUNCTIONS
|
|
671
688
|
def get_routine():
|
|
@@ -700,6 +717,7 @@ class Microwave3D:
|
|
|
700
717
|
results: list[SimJob] = []
|
|
701
718
|
matset: list[tuple[np.ndarray, np.ndarray, np.ndarray]] = []
|
|
702
719
|
|
|
720
|
+
logger.trace(f'Frequency groups: {freq_groups}')
|
|
703
721
|
## Single threaded
|
|
704
722
|
job_id = 1
|
|
705
723
|
|
|
@@ -797,8 +815,8 @@ class Microwave3D:
|
|
|
797
815
|
# Distribute taks
|
|
798
816
|
group_results = pool.map(run_job_multi, jobs)
|
|
799
817
|
results.extend(group_results)
|
|
800
|
-
|
|
801
|
-
|
|
818
|
+
if parallel:
|
|
819
|
+
thread_local.__dict__.clear()
|
|
802
820
|
logger.info('Solving complete')
|
|
803
821
|
|
|
804
822
|
for freq, job in zip(self.frequencies, results):
|
|
@@ -807,7 +825,7 @@ class Microwave3D:
|
|
|
807
825
|
for variables, data in self.data.sim.iterate():
|
|
808
826
|
logger.trace(f'Sim variable: {variables}')
|
|
809
827
|
for item in data['report']:
|
|
810
|
-
item.
|
|
828
|
+
item.logprint(logger.trace)
|
|
811
829
|
|
|
812
830
|
self.solveroutine.reset()
|
|
813
831
|
### Compute S-parameters and return
|
|
@@ -914,8 +932,7 @@ class Microwave3D:
|
|
|
914
932
|
mesh = self.mesh
|
|
915
933
|
all_ports = self.bc.oftype(PortBC)
|
|
916
934
|
port_numbers = [port.port_number for port in all_ports]
|
|
917
|
-
|
|
918
|
-
|
|
935
|
+
|
|
919
936
|
logger.info('Computing S-parameters')
|
|
920
937
|
|
|
921
938
|
|
|
@@ -947,6 +964,8 @@ class Microwave3D:
|
|
|
947
964
|
|
|
948
965
|
# Recording port information
|
|
949
966
|
for active_port in all_ports:
|
|
967
|
+
port_tets = self.mesh.get_face_tets(active_port.tags)
|
|
968
|
+
|
|
950
969
|
fielddata.add_port_properties(active_port.port_number,
|
|
951
970
|
mode_number=active_port.mode_number,
|
|
952
971
|
k0 = k0,
|
|
@@ -969,7 +988,7 @@ class Microwave3D:
|
|
|
969
988
|
fielddata.basis = self.basis
|
|
970
989
|
# Compute the S-parameters
|
|
971
990
|
# Define the field interpolation function
|
|
972
|
-
fieldf = self.basis.interpolate_Ef(solution, tetids=
|
|
991
|
+
fieldf = self.basis.interpolate_Ef(solution, tetids=port_tets)
|
|
973
992
|
Pout = 0.0 + 0j
|
|
974
993
|
|
|
975
994
|
# Active port power
|
|
@@ -981,6 +1000,8 @@ class Microwave3D:
|
|
|
981
1000
|
|
|
982
1001
|
#Passive ports
|
|
983
1002
|
for bc in all_ports:
|
|
1003
|
+
port_tets = self.mesh.get_face_tets(bc.tags)
|
|
1004
|
+
fieldf = self.basis.interpolate_Ef(solution, tetids=port_tets)
|
|
984
1005
|
tris = mesh.get_triangles(bc.tags)
|
|
985
1006
|
tri_vertices = mesh.tris[:,tris]
|
|
986
1007
|
pfield, pmode = self._compute_s_data(bc, fieldf,tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
|
|
@@ -96,6 +96,15 @@ class MWBoundaryConditionSet(BoundaryConditionSet):
|
|
|
96
96
|
self._cell._ports.append(port)
|
|
97
97
|
return port
|
|
98
98
|
|
|
99
|
+
# Checks
|
|
100
|
+
def _is_excited(self) -> bool:
|
|
101
|
+
for bc in self.boundary_conditions:
|
|
102
|
+
if not isinstance(bc, RobinBC):
|
|
103
|
+
continue
|
|
104
|
+
if bc._include_force:
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
return False
|
|
99
108
|
|
|
100
109
|
############################################################
|
|
101
110
|
# BOUNDARY CONDITIONS #
|
|
@@ -703,18 +712,20 @@ class RectangularWaveguide(PortBC):
|
|
|
703
712
|
self.mode: tuple[int,int] = mode
|
|
704
713
|
|
|
705
714
|
if dims is None:
|
|
706
|
-
logger.
|
|
715
|
+
logger.debug(f" - Establishing RectangularWaveguide port face based on selection {self.selection}")
|
|
707
716
|
cs, (width, height) = self.selection.rect_basis() # type: ignore
|
|
708
717
|
self.cs = cs # type: ignore
|
|
709
718
|
self.dims = (width, height)
|
|
710
|
-
logger.debug(f'Port CS: {self.cs}')
|
|
711
|
-
logger.debug(f'Detected port {self.port_number} size = {width*1000:.1f} mm x {height*1000:.1f} mm')
|
|
719
|
+
logger.debug(f' - Port CS: {self.cs}')
|
|
720
|
+
logger.debug(f' - Detected port {self.port_number} size = {width*1000:.1f} mm x {height*1000:.1f} mm')
|
|
712
721
|
else:
|
|
713
722
|
self.dims = dims
|
|
714
723
|
self.cs = cs
|
|
724
|
+
|
|
715
725
|
if self.cs is None:
|
|
716
|
-
logger.info('Constructing coordinate system from normal port')
|
|
726
|
+
logger.info(' - Constructing coordinate system from normal port')
|
|
717
727
|
self.cs = Axis(self.selection.normal).construct_cs()
|
|
728
|
+
logger.debug(f' - Port CS: {self.cs}')
|
|
718
729
|
def get_basis(self) -> np.ndarray:
|
|
719
730
|
return self.cs._basis
|
|
720
731
|
|
|
@@ -20,7 +20,7 @@ from ...simulation_data import BaseDataset, DataContainer
|
|
|
20
20
|
from ...elements.femdata import FEMBasis
|
|
21
21
|
from dataclasses import dataclass
|
|
22
22
|
import numpy as np
|
|
23
|
-
from typing import Literal
|
|
23
|
+
from typing import Literal
|
|
24
24
|
from loguru import logger
|
|
25
25
|
from .adaptive_freq import SparamModel
|
|
26
26
|
from ...cs import Axis, _parse_axis
|
|
@@ -344,7 +344,7 @@ class FarFieldData:
|
|
|
344
344
|
}
|
|
345
345
|
mapping = fmap.get(quantity.lower(),np.abs)
|
|
346
346
|
|
|
347
|
-
F = mapping(self
|
|
347
|
+
F = mapping(getattr(self, polarization))
|
|
348
348
|
|
|
349
349
|
if isotropic:
|
|
350
350
|
F = F/np.sqrt(Z0/(2*np.pi))
|
|
@@ -700,7 +700,7 @@ class MWField:
|
|
|
700
700
|
''' Return the magnetic field as a tuple of numpy arrays '''
|
|
701
701
|
return self.Hx, self.Hy, self.Hz
|
|
702
702
|
|
|
703
|
-
def interpolate(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray) -> EHField:
|
|
703
|
+
def interpolate(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray, usenan: bool = True) -> EHField:
|
|
704
704
|
''' Interpolate the dataset in the provided xs, ys, zs values'''
|
|
705
705
|
if isinstance(xs, (float, int, complex)):
|
|
706
706
|
xs = np.array([xs,])
|
|
@@ -711,15 +711,16 @@ class MWField:
|
|
|
711
711
|
xf = xs.flatten()
|
|
712
712
|
yf = ys.flatten()
|
|
713
713
|
zf = zs.flatten()
|
|
714
|
-
Ex, Ey, Ez = self.basis.interpolate(self._field, xf, yf, zf)
|
|
714
|
+
Ex, Ey, Ez = self.basis.interpolate(self._field, xf, yf, zf, usenan=usenan)
|
|
715
715
|
self.Ex = Ex.reshape(shp)
|
|
716
716
|
self.Ey = Ey.reshape(shp)
|
|
717
717
|
self.Ez = Ez.reshape(shp)
|
|
718
718
|
|
|
719
719
|
|
|
720
720
|
constants = 1/ (-1j*2*np.pi*self.freq*(self._dur*MU0) )
|
|
721
|
-
Hx, Hy, Hz = self.basis.interpolate_curl(self._field, xf, yf, zf, constants)
|
|
721
|
+
Hx, Hy, Hz = self.basis.interpolate_curl(self._field, xf, yf, zf, constants, usenan=usenan)
|
|
722
722
|
ids = self.basis.interpolate_index(xf, yf, zf)
|
|
723
|
+
|
|
723
724
|
self.er = self._der[ids].reshape(shp)
|
|
724
725
|
self.ur = self._dur[ids].reshape(shp)
|
|
725
726
|
self.Hx = Hx.reshape(shp)
|
|
@@ -735,7 +736,8 @@ class MWField:
|
|
|
735
736
|
ds: float,
|
|
736
737
|
x: float | None = None,
|
|
737
738
|
y: float | None = None,
|
|
738
|
-
z: float | None = None
|
|
739
|
+
z: float | None = None,
|
|
740
|
+
usenan: bool = True) -> EHField:
|
|
739
741
|
"""Create a cartesian cut plane (XY, YZ or XZ) and compute the E and H-fields there
|
|
740
742
|
|
|
741
743
|
Only one coordiante and thus cutplane may be defined. If multiple are defined only the last (x->y->z) is used.
|
|
@@ -763,12 +765,13 @@ class MWField:
|
|
|
763
765
|
if z is not None:
|
|
764
766
|
X,Y = np.meshgrid(xs, ys)
|
|
765
767
|
Z = z*np.ones_like(Y)
|
|
766
|
-
return self.interpolate(X,Y,Z)
|
|
768
|
+
return self.interpolate(X,Y,Z, usenan=usenan)
|
|
767
769
|
|
|
768
770
|
def cutplane_normal(self,
|
|
769
771
|
point=(0,0,0),
|
|
770
772
|
normal=(0,0,1),
|
|
771
|
-
npoints: int = 300
|
|
773
|
+
npoints: int = 300,
|
|
774
|
+
usenan: bool = True) -> EHField:
|
|
772
775
|
"""
|
|
773
776
|
Take a 2D slice of the field along an arbitrary plane.
|
|
774
777
|
Args:
|
|
@@ -812,10 +815,10 @@ class MWField:
|
|
|
812
815
|
Y = point[1] + S_mesh*u[1] + T_mesh*v[1]
|
|
813
816
|
Z = point[2] + S_mesh*u[2] + T_mesh*v[2]
|
|
814
817
|
|
|
815
|
-
return self.interpolate(X, Y, Z)
|
|
818
|
+
return self.interpolate(X, Y, Z, usenan=usenan)
|
|
816
819
|
|
|
817
820
|
|
|
818
|
-
def grid(self, ds: float) -> EHField:
|
|
821
|
+
def grid(self, ds: float, usenan: bool = True) -> EHField:
|
|
819
822
|
"""Interpolate a uniform grid sampled at ds
|
|
820
823
|
|
|
821
824
|
Args:
|
|
@@ -829,7 +832,7 @@ class MWField:
|
|
|
829
832
|
ys = np.linspace(yb[0], yb[1], int((yb[1]-yb[0])/ds))
|
|
830
833
|
zs = np.linspace(zb[0], zb[1], int((zb[1]-zb[0])/ds))
|
|
831
834
|
X, Y, Z = np.meshgrid(xs, ys, zs)
|
|
832
|
-
return self.interpolate(X,Y,Z)
|
|
835
|
+
return self.interpolate(X,Y,Z, usenan=usenan)
|
|
833
836
|
|
|
834
837
|
def vector(self, field: Literal['E','H'], metric: Literal['real','imag','complex'] = 'real') -> tuple[np.ndarray, np.ndarray,np.ndarray,np.ndarray,np.ndarray,np.ndarray]:
|
|
835
838
|
"""Returns the X,Y,Z,Fx,Fy,Fz data to be directly cast into plot functions.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Iterable, Sequence, Optional
|
|
2
|
+
import numpy as np
|
|
3
|
+
from matplotlib.colors import LinearSegmentedColormap, to_rgba
|
|
4
|
+
|
|
5
|
+
def make_colormap(
|
|
6
|
+
hex_colors: Sequence[str],
|
|
7
|
+
positions: Optional[Iterable[float]] = None,
|
|
8
|
+
name: str = "custom",
|
|
9
|
+
N: int = 256,
|
|
10
|
+
) -> LinearSegmentedColormap:
|
|
11
|
+
"""
|
|
12
|
+
Create a Matplotlib colormap from hex colors with optional positions.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
hex_colors : sequence of str
|
|
17
|
+
Hex color strings like '#RRGGBB' (or 'RRGGBB'). At least 2 required.
|
|
18
|
+
positions : iterable of float in [0, 1], optional
|
|
19
|
+
Locations of each color along the gradient. If not provided,
|
|
20
|
+
they are evenly spaced via linspace(0, 1, len(hex_colors)).
|
|
21
|
+
If provided, they do not have to be sorted; they will be sorted
|
|
22
|
+
together with the colors. If the first/last position do not
|
|
23
|
+
hit 0 or 1, they’ll be extended by duplicating the end colors.
|
|
24
|
+
name : str
|
|
25
|
+
Name for the colormap.
|
|
26
|
+
N : int
|
|
27
|
+
Resolution (number of lookup entries) of the colormap.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
matplotlib.colors.LinearSegmentedColormap
|
|
32
|
+
"""
|
|
33
|
+
# Normalize hex strings and basic validation
|
|
34
|
+
colors = []
|
|
35
|
+
for c in hex_colors:
|
|
36
|
+
if not isinstance(c, str):
|
|
37
|
+
raise TypeError("All colors must be hex strings.")
|
|
38
|
+
colors.append(c if c.startswith("#") else f"#{c}")
|
|
39
|
+
|
|
40
|
+
if len(colors) < 2:
|
|
41
|
+
raise ValueError("Provide at least two hex colors.")
|
|
42
|
+
|
|
43
|
+
# Build/validate positions
|
|
44
|
+
if positions is None:
|
|
45
|
+
pos = np.linspace(0.0, 1.0, len(colors), dtype=float)
|
|
46
|
+
else:
|
|
47
|
+
pos = np.asarray(list(positions), dtype=float)
|
|
48
|
+
if pos.size != len(colors):
|
|
49
|
+
raise ValueError("`positions` must have the same length as `hex_colors`.")
|
|
50
|
+
if np.any((pos < 0.0) | (pos > 1.0)):
|
|
51
|
+
raise ValueError("All positions must be within [0, 1].")
|
|
52
|
+
|
|
53
|
+
# Sort positions and carry colors along
|
|
54
|
+
order = np.argsort(pos)
|
|
55
|
+
pos = pos[order]
|
|
56
|
+
colors = [colors[i] for i in order]
|
|
57
|
+
|
|
58
|
+
# Ensure coverage of [0, 1] by extending ends if needed
|
|
59
|
+
if pos[0] > 0.0:
|
|
60
|
+
pos = np.r_[0.0, pos]
|
|
61
|
+
colors = [colors[0]] + colors
|
|
62
|
+
if pos[-1] < 1.0:
|
|
63
|
+
pos = np.r_[pos, 1.0]
|
|
64
|
+
colors = colors + [colors[-1]]
|
|
65
|
+
|
|
66
|
+
# Pair positions with RGBA
|
|
67
|
+
segment = list(zip(pos.tolist(), (to_rgba(c) for c in colors)))
|
|
68
|
+
|
|
69
|
+
# Create the colormap
|
|
70
|
+
return LinearSegmentedColormap.from_list(name, segment, N=N)
|