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.

Files changed (30) hide show
  1. emerge/__init__.py +7 -3
  2. emerge/_emerge/elements/femdata.py +5 -1
  3. emerge/_emerge/elements/ned2_interp.py +73 -30
  4. emerge/_emerge/elements/nedelec2.py +1 -0
  5. emerge/_emerge/emerge_update.py +63 -0
  6. emerge/_emerge/geo/operations.py +2 -1
  7. emerge/_emerge/geo/polybased.py +26 -5
  8. emerge/_emerge/geometry.py +5 -0
  9. emerge/_emerge/logsettings.py +26 -1
  10. emerge/_emerge/material.py +29 -8
  11. emerge/_emerge/mesh3d.py +16 -13
  12. emerge/_emerge/mesher.py +70 -3
  13. emerge/_emerge/physics/microwave/assembly/assembler.py +5 -4
  14. emerge/_emerge/physics/microwave/assembly/curlcurl.py +0 -1
  15. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +1 -2
  16. emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +1 -1
  17. emerge/_emerge/physics/microwave/assembly/robin_abc_order2.py +0 -1
  18. emerge/_emerge/physics/microwave/microwave_3d.py +37 -16
  19. emerge/_emerge/physics/microwave/microwave_bc.py +15 -4
  20. emerge/_emerge/physics/microwave/microwave_data.py +14 -11
  21. emerge/_emerge/plot/pyvista/cmap_maker.py +70 -0
  22. emerge/_emerge/plot/pyvista/display.py +101 -37
  23. emerge/_emerge/simmodel.py +75 -21
  24. emerge/_emerge/simulation_data.py +22 -4
  25. emerge/_emerge/solver.py +78 -51
  26. {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/METADATA +2 -3
  27. {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/RECORD +30 -28
  28. {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/WHEEL +0 -0
  29. {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/entry_points.txt +0 -0
  30. {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
- def set_mesh_size(self, discretizer: Callable, resolution: float):
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
- logger.info(f'Setting mesh size for {_DOM_TO_STR[obj.dim]} domain with tags {obj.tags} to {1000*size:.3f}mm')
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, 1e-9)
365
- linked_edges = pair_coordinates(mesh.edge_centers, edge_ids_1, edge_ids_2, dv, 1e-9)
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, 1e-9)
551
- linked_edges = pair_coordinates(mesh.edge_centers, edge_ids_1, edge_ids_2, dv, 1e-9)
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
 
@@ -160,7 +160,6 @@ def tet_mass_stiffness_matrices(field: Nedelec2,
160
160
  edges = field.mesh.edges
161
161
  nodes = field.mesh.nodes
162
162
 
163
- nT = tets.shape[1]
164
163
  tet_to_field = field.tet_to_field
165
164
  tet_to_edge = field.mesh.tet_to_edge
166
165
  nE = edges.shape[1]
@@ -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, gaus_quad_tri
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, gaus_quad_tri
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
 
@@ -18,7 +18,6 @@
18
18
 
19
19
  import numpy as np
20
20
  from ....elements import Nedelec2
21
- from scipy.sparse import csr_matrix
22
21
  from ....mth.optimized import local_mapping, compute_distances
23
22
  from numba import c16, types, f8, i8, njit, prange
24
23
 
@@ -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.conductivity_limit = condutivity
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 temporarily not supported')
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
- logger.debug('Finding Lumped Port integration points')
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('Retreiving material properties.')
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
- # Thread-local storage for per-thread resources
668
- thread_local = threading.local()
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
- thread_local.__dict__.clear()
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.pretty_print(logger.trace)
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
- all_port_tets = self.mesh.get_face_tets(*[port.tags for port in all_ports])
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=all_port_tets)
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.info("Determining port face based on selection")
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, Callable
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.__dict__.get(polarization, self.normE))
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) -> EHField:
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) -> EHField:
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)