emerge 1.0.6__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of emerge might be problematic. Click here for more details.

@@ -29,6 +29,7 @@ from ...bc import BoundaryCondition, BoundaryConditionSet, Periodic
29
29
  from ...periodic import PeriodicCell, HexCell, RectCell
30
30
  from ...material import Material
31
31
  from ...const import Z0, C0, EPS0, MU0
32
+ from ...logsettings import DEBUG_COLLECTOR
32
33
 
33
34
  ############################################################
34
35
  # UTILITY FUNCTIONS #
@@ -59,6 +60,7 @@ class MWBoundaryConditionSet(BoundaryConditionSet):
59
60
  self.RectangularWaveguide: type[RectangularWaveguide] = self._construct_bc(RectangularWaveguide)
60
61
  self.Periodic: type[Periodic] = self._construct_bc(Periodic)
61
62
  self.FloquetPort: type[FloquetPort] = self._construct_bc(FloquetPort)
63
+ self.UserDefinedPort: type[UserDefinedPort] = self._construct_bc(UserDefinedPort)
62
64
 
63
65
  self._cell: PeriodicCell | None = None
64
66
 
@@ -73,7 +75,6 @@ class MWBoundaryConditionSet(BoundaryConditionSet):
73
75
  for bc in self.oftype(SurfaceImpedance):
74
76
  if bc.sigma > 10.0:
75
77
  bcs.append(bc)
76
-
77
78
  return bcs
78
79
 
79
80
  def get_type(self, bctype: Literal['PEC','ModalPort','LumpedPort','PMC','LumpedElement','RectangularWaveguide','Periodic','FloquetPort','SurfaceImpedance']) -> FaceSelection:
@@ -342,7 +343,6 @@ class PortMode:
342
343
  norm_factor: float = 1
343
344
  freq: float = 0
344
345
  neff: float = 1
345
- TEM: bool = True
346
346
  Z0: float = 50.0
347
347
  polarity: float = 1.0
348
348
  modetype: Literal['TEM','TE','TM'] = 'TEM'
@@ -460,7 +460,7 @@ class ModalPort(PortBC):
460
460
  port_number: int,
461
461
  cs: CoordinateSystem | None = None,
462
462
  power: float = 1,
463
- TEM: bool = False,
463
+ modetype: Literal['TE','TM','TEM'] | None = None,
464
464
  mixed_materials: bool = False):
465
465
  """Generes a ModalPort boundary condition for a port that requires eigenmode solutions for the mode.
466
466
 
@@ -477,7 +477,7 @@ class ModalPort(PortBC):
477
477
  port_number (int): The port number as an integer
478
478
  cs (CoordinateSystem, optional): The local coordinate system of the port face. Defaults to None.
479
479
  power (float, optional): The radiated power. Defaults to 1.
480
- TEM (bool, optional): Wether the mode should be considered as a TEM mode. Defaults to False
480
+ modetype (str[TE, TM, TEM], optional): Wether the mode should be considered as a TEM mode. Defaults to False
481
481
  mixed_materials (bool, optional): Wether the port consists of multiple different dielectrics. This requires
482
482
  A recalculation of the port mode at every frequency
483
483
  """
@@ -491,12 +491,14 @@ class ModalPort(PortBC):
491
491
  self.selected_mode: int = 0
492
492
  self.modes: dict[float, list[PortMode]] = defaultdict(list)
493
493
 
494
- self.TEM: bool = TEM
494
+ self.forced_modetype: Literal['TE','TM','TEM'] | None = modetype
495
495
  self.mixed_materials: bool = mixed_materials
496
496
  self.initialized: bool = False
497
497
  self._first_k0: float | None = None
498
498
  self._last_k0: float | None = None
499
499
 
500
+ self.plus_terminal: list[tuple[int, int]] = []
501
+ self.minus_terminal: list[tuple[int, int]] = []
500
502
 
501
503
  if cs is None:
502
504
  logger.info('Constructing coordinate system from normal port')
@@ -505,7 +507,25 @@ class ModalPort(PortBC):
505
507
  raise ValueError('No Coordinate System could be derived.')
506
508
  self._er: np.ndarray | None = None
507
509
  self._ur: np.ndarray | None = None
510
+
511
+ self.vintline: list[Line] = []
512
+
513
+ def set_integration_line(self, c1: tuple[float, float, float], c2: tuple[float, float, float], N: int = 21) -> None:
514
+ """Define the integration line start and end point
515
+
516
+ Args:
517
+ c1 (tuple[float, float, float]): The start coordinate
518
+ c2 (tuple[float, float, float]): The end coordinate
519
+ N (int, optional): The number of integration points. Defaults to 21.
520
+ """
521
+ self.vintline.append(Line.from_points(c1, c2, N))
508
522
 
523
+ def reset(self) -> None:
524
+ self.modes: dict[float, list[PortMode]] = defaultdict(list)
525
+ self.initialized: bool = False
526
+ self.plus_terminal: list[tuple[int, int]] = []
527
+ self.minus_terminal: list[tuple[int, int]] = []
528
+
509
529
  def portZ0(self, k0: float) -> complex | float | None:
510
530
  return self.get_mode(k0).Z0
511
531
 
@@ -522,6 +542,31 @@ class ModalPort(PortBC):
522
542
  *axes (tuple, np.ndarray, Axis): The alignment vectors.
523
543
  """
524
544
  self.alignment_vectors = [_parse_axis(ax) for ax in axes]
545
+
546
+ def _get_alignment_vector(self, index: int) -> np.ndarray | None:
547
+ if len(self.alignment_vectors) > index:
548
+ return self.alignment_vectors[index].np
549
+ return None
550
+
551
+ def set_terminals(self, positive: Selection | GeoObject | None = None,
552
+ negative: Selection | GeoObject | None = None,
553
+ ground: Selection | GeoObject | None = None) -> None:
554
+ """Define which objects/faces/selection should be assigned the positive terminal
555
+ and which one the negative terminal.
556
+
557
+ The terminal assignment will be used to find an integration line for the impedance calculation.
558
+
559
+ Note: Ground is currently unused.
560
+
561
+ Args:
562
+ positive (Selection | GeoObject | None, optional): The postive terminal. Defaults to None.
563
+ negative (Selection | GeoObject | None, optional): The negative terminal. Defaults to None.
564
+ ground (Selection | GeoObject | None, optional): _description_. Defaults to None.
565
+ """
566
+ if positive is not None:
567
+ self.plus_terminal = positive.dimtags
568
+ if negative is not None:
569
+ self.minus_terminal = negative.dimtags
525
570
 
526
571
  @property
527
572
  def nmodes(self) -> int:
@@ -569,9 +614,10 @@ class ModalPort(PortBC):
569
614
  Returns:
570
615
  PortMode: The requested PortMode object
571
616
  """
617
+ options = self.modes[min(self.modes.keys(), key=lambda k: abs(k - k0))]
572
618
  if i is None:
573
- i = self.selected_mode
574
- return self.modes[min(self.modes.keys(), key=lambda k: abs(k - k0))][i]
619
+ i = min(len(options)-1, self.selected_mode)
620
+ return options[i]
575
621
 
576
622
  def global_field_function(self, k0: float = 0, which: Literal['E','H'] = 'E') -> Callable:
577
623
  ''' The field function used to compute the E-field.
@@ -594,7 +640,7 @@ class ModalPort(PortBC):
594
640
  beta: float,
595
641
  k0: float,
596
642
  residual: float,
597
- TEM: bool,
643
+ number: int,
598
644
  freq: float) -> PortMode | None:
599
645
  """Add a mode function to the ModalPort
600
646
 
@@ -605,16 +651,17 @@ class ModalPort(PortBC):
605
651
  beta (float): The out-of-plane propagation constant
606
652
  k0 (float): The free space phase constant
607
653
  residual (float): The solution residual
608
- TEM (bool): Whether its a TEM mode
609
654
  freq (float): The frequency of the port mode
610
655
 
611
656
  Returns:
612
657
  PortMode: The port mode object.
613
658
  """
614
- mode = PortMode(field, E_function, H_function, k0, beta, residual, TEM=TEM, freq=freq)
659
+ mode = PortMode(field, E_function, H_function, k0, beta, residual, freq=freq)
660
+
615
661
  if mode.energy < 1e-4:
616
662
  logger.debug(f'Ignoring mode due to a low mode energy: {mode.energy}')
617
663
  return None
664
+
618
665
  self.modes[k0].append(mode)
619
666
  self.initialized = True
620
667
 
@@ -637,7 +684,7 @@ class ModalPort(PortBC):
637
684
 
638
685
  def get_beta(self, k0: float) -> float:
639
686
  mode = self.get_mode(k0)
640
- if mode.TEM:
687
+ if self.forced_modetype=='TEM':
641
688
  beta = mode.beta/mode.k0 * k0
642
689
  else:
643
690
  freq = k0*299792458/(2*np.pi)
@@ -794,6 +841,125 @@ class RectangularWaveguide(PortBC):
794
841
  Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
795
842
  return np.array([Exg, Eyg, Ezg])
796
843
 
844
+ def _f_zero(k0,x,y,z):
845
+ "Zero field function"
846
+ return np.zeros_like(x, dtype=np.complex128)
847
+
848
+ class UserDefinedPort(PortBC):
849
+
850
+ _include_stiff: bool = True
851
+ _include_mass: bool = False
852
+ _include_force: bool = True
853
+
854
+ def __init__(self,
855
+ face: FaceSelection | GeoSurface,
856
+ port_number: int,
857
+ Ex: Callable | None = None,
858
+ Ey: Callable | None = None,
859
+ Ez: Callable | None = None,
860
+ kz: Callable | None = None,
861
+ power: float = 1.0,
862
+ modetype: Literal['TEM','TE','TM'] = 'TEM',
863
+ cs: CoordinateSystem | None = None):
864
+ """Creates a user defined port field
865
+
866
+ The UserDefinedPort is defined based on user defined field callables. All undefined callables will default to 0 field or k0.
867
+
868
+ All spatial field functions should be defined using the template:
869
+ >>> def Ec(k0: float, x: np.ndarray, y: np.ndarray, z: np.ndarray) -> np.ndarray
870
+ >>> return #shape like x
871
+
872
+ Args:
873
+ face (FaceSelection, GeoSurface): The port boundary face selection
874
+ port_number (int): The port number
875
+ Ex (Callable): The Ex(k0,x,y,z) field
876
+ Ey (Callable): The Ey(k0,x,y,z) field
877
+ Ez (Callable): The Ez(k0,x,y,z) field
878
+ kz (Callable): The out of plane propagation constant kz(k0)
879
+ power (float): The port output power
880
+ """
881
+ super().__init__(face)
882
+ if cs is None:
883
+ cs = GCS
884
+
885
+ self.cs = cs
886
+ self.port_number: int= port_number
887
+ self.active: bool = False
888
+ self.power: float = power
889
+ self.type: str = 'TE'
890
+ if Ex is None:
891
+ Ex = _f_zero
892
+ if Ey is None:
893
+ Ey = _f_zero
894
+ if Ez is None:
895
+ Ez = _f_zero
896
+ if kz is None:
897
+ kz = lambda k0: k0
898
+
899
+ self._fex: Callable = Ex
900
+ self._fey: Callable = Ey
901
+ self._fez: Callable = Ez
902
+ self._fkz: Callable = kz
903
+ self.type = modetype
904
+
905
+ def get_basis(self) -> np.ndarray:
906
+ return self.cs._basis
907
+
908
+ def get_inv_basis(self) -> np.ndarray:
909
+ return self.cs._basis_inv
910
+
911
+ def modetype(self, k0):
912
+ return self.type
913
+
914
+ def get_amplitude(self, k0: float) -> float:
915
+ return np.sqrt(self.power)
916
+
917
+ def get_beta(self, k0: float) -> float:
918
+ ''' Return the out of plane propagation constant. βz.'''
919
+ return self._fkz(k0)
920
+
921
+ def get_gamma(self, k0: float) -> complex:
922
+ """Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
923
+
924
+ Args:
925
+ k0 (float): The free space propagation constant.
926
+
927
+ Returns:
928
+ complex: The γ-constant
929
+ """
930
+ return 1j*self.get_beta(k0)
931
+
932
+ def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0: float) -> np.ndarray:
933
+ return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
934
+
935
+ def port_mode_3d(self,
936
+ x_local: np.ndarray,
937
+ y_local: np.ndarray,
938
+ k0: float,
939
+ which: Literal['E','H'] = 'E') -> np.ndarray:
940
+ x_global, y_global, z_global = self.cs.in_global_cs(x_local, y_local, 0*x_local)
941
+
942
+ Egxyz = self.port_mode_3d_global(x_global,y_global,z_global,k0,which=which)
943
+
944
+ Ex, Ey, Ez = self.cs.in_local_basis(Egxyz[0,:], Egxyz[1,:], Egxyz[2,:])
945
+
946
+ Exyz = np.array([Ex, Ey, Ez])
947
+ return Exyz
948
+
949
+ def port_mode_3d_global(self,
950
+ x_global: np.ndarray,
951
+ y_global: np.ndarray,
952
+ z_global: np.ndarray,
953
+ k0: float,
954
+ which: Literal['E','H'] = 'E') -> np.ndarray:
955
+ '''Compute the port mode field for global xyz coordinates.'''
956
+ xl, yl, _ = self.cs.in_local_cs(x_global, y_global, z_global)
957
+ Ex = self._fex(k0, x_global, y_global, z_global)
958
+ Ey = self._fey(k0, x_global, y_global, z_global)
959
+ Ez = self._fez(k0, x_global, y_global, z_global)
960
+ Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
961
+ return np.array([Exg, Eyg, Ezg])
962
+
797
963
  class LumpedPort(PortBC):
798
964
 
799
965
  _include_stiff: bool = True
@@ -852,6 +1018,11 @@ class LumpedPort(PortBC):
852
1018
  self.vintline: list[Line] = []
853
1019
  self.v_integration = True
854
1020
 
1021
+ # Sanity checks
1022
+ if self.width > 0.5 or self.height > 0.5:
1023
+ DEBUG_COLLECTOR.add_report(f'{self}: A lumped port width/height larger than 0.5m has been detected: width={self.width:.3f}m. Height={self.height:.3f}.m. Perhaps you forgot a unit like mm, um, or mil')
1024
+
1025
+
855
1026
  @property
856
1027
  def surfZ(self) -> float:
857
1028
  """The surface sheet impedance for the lumped port
@@ -18,7 +18,7 @@
18
18
  from __future__ import annotations
19
19
  from ...simulation_data import BaseDataset, DataContainer
20
20
  from ...elements.femdata import FEMBasis
21
- from dataclasses import dataclass
21
+ from dataclasses import dataclass, field
22
22
  import numpy as np
23
23
  from typing import Literal
24
24
  from loguru import logger
@@ -372,6 +372,7 @@ class EHField:
372
372
  freq: float
373
373
  er: np.ndarray
374
374
  ur: np.ndarray
375
+ aux: dict[str, np.ndarray] = field(default_factory=dict)
375
376
 
376
377
  @property
377
378
  def k0(self) -> float:
@@ -537,7 +538,7 @@ class EHField:
537
538
 
538
539
  return self.x, self.y, self.z, Fx, Fy, Fz
539
540
 
540
- def scalar(self, field: Literal['Ex','Ey','Ez','Hx','Hy','Hz','normE','normH'], metric: Literal['abs','real','imag','complex'] = 'real') -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
541
+ def scalar(self, field: Literal['Ex','Ey','Ez','Hx','Hy','Hz','normE','normH'] | str, metric: Literal['abs','real','imag','complex'] = 'real') -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
541
542
  """Returns the data X, Y, Z, Field based on the interpolation
542
543
 
543
544
  For animations, make sure to select the complex metric.
@@ -549,7 +550,11 @@ class EHField:
549
550
  Returns:
550
551
  (X,Y,Z,Field): The coordinates plus field scalar
551
552
  """
552
- field_arry = getattr(self, field)
553
+ if field in self.aux:
554
+ field_arry = self.aux[field]
555
+ else:
556
+ field_arry = getattr(self, field)
557
+
553
558
  if metric=='abs':
554
559
  field = np.abs(field_arry)
555
560
  elif metric=='real':
@@ -666,6 +671,10 @@ class MWField:
666
671
  self.excitation = {key: 0.0 for key in self._fields.keys()}
667
672
  self.excitation[self.port_modes[0].port_number] = 1.0 + 0j
668
673
 
674
+ def excite_port(self, number: int) -> None:
675
+ self.excitation = {key: 0.0 for key in self._fields.keys()}
676
+ self.excitation[self.port_modes[number].port_number] = 1.0 + 0j
677
+
669
678
  @property
670
679
  def EH(self) -> tuple[np.ndarray, np.ndarray]:
671
680
  ''' Return the electric and magnetic field as a tuple of numpy arrays '''
@@ -726,12 +735,20 @@ class MWField:
726
735
  self.Hx = Hx.reshape(shp)
727
736
  self.Hy = Hy.reshape(shp)
728
737
  self.Hz = Hz.reshape(shp)
729
-
738
+
730
739
  self._x = xs
731
740
  self._y = ys
732
741
  self._z = zs
733
- return EHField(xs, ys, zs, self.Ex, self.Ey, self.Ez, self.Hx, self.Hy, self.Hz, self.freq, self.er, self.ur)
734
-
742
+ field = EHField(xs, ys, zs, self.Ex, self.Ey, self.Ez, self.Hx, self.Hy, self.Hz, self.freq, self.er, self.ur)
743
+
744
+ return field
745
+
746
+ def _solution_quality(self) -> tuple[np.ndarray, np.ndarray]:
747
+ from .adaptive_mesh import compute_error_estimate
748
+
749
+ error_tet, max_elem_size = compute_error_estimate(self)
750
+ return error_tet, max_elem_size
751
+
735
752
  def boundary(self,
736
753
  selection: FaceSelection) -> EHField:
737
754
  nodes = self.mesh.nodes
@@ -15,20 +15,21 @@
15
15
  # along with this program; if not, see
16
16
  # <https://www.gnu.org/licenses/>.
17
17
  from __future__ import annotations
18
- import time
19
18
  from ...mesh3d import Mesh3D
19
+ from ...simstate import SimState
20
20
  from ...geometry import GeoObject
21
21
  from ...selection import FaceSelection, DomainSelection, EdgeSelection, Selection, encode_data
22
22
  from ...physics.microwave.microwave_bc import PortBC, ModalPort
23
- import numpy as np
24
- import pyvista as pv
25
- from typing import Iterable, Literal, Callable, Any
26
23
  from ..display import BaseDisplay
27
24
  from .display_settings import PVDisplaySettings
28
- from matplotlib.colors import ListedColormap
29
25
  from .cmap_maker import make_colormap
30
26
 
27
+ import time
28
+ import numpy as np
29
+ import pyvista as pv
30
+ from typing import Iterable, Literal, Callable, Any
31
31
  from itertools import cycle
32
+ from loguru import logger
32
33
  ### Color scale
33
34
 
34
35
  # Define the colors we want to use
@@ -232,8 +233,8 @@ class _AnimObject:
232
233
 
233
234
  class PVDisplay(BaseDisplay):
234
235
 
235
- def __init__(self, mesh: Mesh3D):
236
- self._mesh: Mesh3D = mesh
236
+ def __init__(self, state: SimState):
237
+ self._state: SimState = state
237
238
  self.set: PVDisplaySettings = PVDisplaySettings()
238
239
 
239
240
  # Animation options
@@ -260,6 +261,9 @@ class PVDisplay(BaseDisplay):
260
261
  self._cbar_lim: tuple[float, float] | None = None
261
262
  self.camera_position = (1, -1, 1) # +X, +Z, -Y
262
263
 
264
+ @property
265
+ def _mesh(self) -> Mesh3D:
266
+ return self._state.mesh
263
267
 
264
268
  def cbar(self, name: str, n_labels: int = 5, interactive: bool = False, clim: tuple[float, float] | None = None ) -> PVDisplay:
265
269
  self._cbar_args = dict(title=name, n_labels=n_labels, interactive=interactive)
@@ -302,8 +306,7 @@ class PVDisplay(BaseDisplay):
302
306
  self._ruler.min_length = max(1e-3, min(self._mesh.edge_lengths))
303
307
  self._update_camera()
304
308
  self._add_aux_items()
305
- # self._plot.renderer.enable_depth_peeling(20, 0.8)
306
- # self._plot.enable_anti_aliasing(self.set.anti_aliassing)
309
+ self._add_background()
307
310
  if self._do_animate:
308
311
  self._wire_close_events()
309
312
  self.add_text('Press Q to close!',color='red', position='upper_left')
@@ -313,14 +316,17 @@ class PVDisplay(BaseDisplay):
313
316
  self._plot.show()
314
317
 
315
318
  self._reset()
316
-
317
- def set_mesh(self, mesh: Mesh3D):
318
- """Define the mesh to be used
319
319
 
320
- Args:
321
- mesh (Mesh3D): The mesh object
322
- """
323
- self._mesh = mesh
320
+ def _add_background(self):
321
+ from pyvista import examples
322
+ from requests.exceptions import ConnectionError
323
+
324
+ try:
325
+ cubemap = examples.download_sky_box_cube_map()
326
+ self._plot.set_environment_texture(cubemap)
327
+ except ConnectionError:
328
+ logger.warning(f'No internet, no background texture will be used.')
329
+
324
330
 
325
331
  def _reset(self):
326
332
  """ Resets key display parameters."""
@@ -456,6 +462,10 @@ class PVDisplay(BaseDisplay):
456
462
  ## OBLIGATORY METHODS
457
463
  def add_object(self, obj: GeoObject | Selection, mesh: bool = False, volume_mesh: bool = True, label: bool = False, *args, **kwargs):
458
464
 
465
+ if isinstance(obj, GeoObject):
466
+ if obj._hidden:
467
+ return
468
+
459
469
  show_edges = False
460
470
  opacity = obj.opacity
461
471
  line_width = 0.5
@@ -539,7 +549,7 @@ class PVDisplay(BaseDisplay):
539
549
  XYZ=None,
540
550
  field: Literal['E','H'] = 'E',
541
551
  k0: float | None = None,
542
- mode_number: int = 0) -> None:
552
+ mode_number: int | None = None) -> None:
543
553
 
544
554
  if XYZ:
545
555
  X,Y,Z = XYZ
@@ -584,7 +594,10 @@ class PVDisplay(BaseDisplay):
584
594
  k0 = port.get_mode(0).k0
585
595
  else:
586
596
  k0 = 1
587
- port.selected_mode = mode_number
597
+
598
+ if isinstance(mode_number, int):
599
+ port.selected_mode = mode_number
600
+
588
601
  F = port.port_mode_3d_global(xf,yf,zf,k0, which=field)
589
602
 
590
603
  Fx = F[0,:].reshape(X.shape).T
@@ -1,12 +1,130 @@
1
+ # EMerge is an open source Python based FEM EM simulation module.
2
+ # Copyright (C) 2025 Robert Fennis.
3
+
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 2
7
+ # of the License, or (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see
16
+ # <https://www.gnu.org/licenses/>.
1
17
 
2
- from typing import Literal
3
18
 
4
19
  class Settings:
5
-
6
20
  def __init__(self):
7
- self.mw_2dbc: bool = True
8
- self.mw_2dbc_lim: float = 10.0
9
- self.mw_2dbc_peclim: float = 1e8
10
- self.mw_3d_peclim: float = 1e7
21
+ self._mw_2dbc: bool = True
22
+ self._mw_2dbc_lim: float = 10.0
23
+ self._mw_2dbc_peclim: float = 1e8
24
+ self._mw_3d_peclim: float = 1e7
25
+ self._mw_cap_sp_single: bool = True
26
+ self._mw_cap_sp_col: bool = True
27
+ self._mw_recip_sp: bool = False
28
+ self._size_check: bool = True
29
+
30
+
31
+
32
+ ############################################################
33
+ # GETTERS #
34
+ ############################################################
35
+
36
+ @property
37
+ def mw_2dbc(self) -> bool:
38
+ """ This variable determines is 2D boundary conditions will be automatically assigned based on material properties.
39
+ """
40
+ return self._mw_2dbc
41
+
42
+ @property
43
+ def mw_2dbc_lim(self) -> float:
44
+ """This variable is the bulk conductivity limit in S/m beyond which a surface material will automatically be assigned as a SurfaceImpedance boundary condition."""
45
+ return self._mw_2dbc_lim
46
+
47
+ @property
48
+ def mw_2dbc_peclim(self) -> float:
49
+ """This variable determines a bulk conductivity limit in S/m beyond which a conductor is assigned PEC instead of a SurfaceImpedance boundary condition."""
50
+ return self._mw_2dbc_peclim
51
+
52
+ @property
53
+ def mw_3d_peclim(self) -> float:
54
+ """This variable determines if bulk conductors with a bulk conductivity beyond a limit (.mw_3d_peclim) are considered PEC.
55
+
56
+ """
57
+ return self._mw_3d_peclim
58
+
59
+ @property
60
+ def size_check(self) -> bool:
61
+ """If a total volume check should be considered (100,000 tetrahedra) to hard crash the simulation assuming that the problem size will be too high to solver.
62
+ 100.000 Tetrahedra would yield approximately 700k Degrees of Freedom
63
+ """
64
+ return self._size_check
65
+
66
+ @property
67
+ def mw_cap_sp_single(self) -> bool:
68
+ """If Single S-parameters should be capped with their magnitude to at most 1.0"""
69
+ return self._mw_cap_sp_single
70
+
71
+ @property
72
+ def mw_cap_sp_col(self) -> bool:
73
+ """If Single S-parameters columns should be power normalized to 1.0"""
74
+ return self._mw_cap_sp_col
75
+
76
+ @property
77
+ def mw_recip_sp(self) -> bool:
78
+ """If reciprodicty should be explicitly enforced"""
79
+ return self._mw_recip_sp
80
+ ############################################################
81
+ # SETTERS #
82
+ ############################################################
83
+
84
+ @mw_2dbc.setter
85
+ def mw_2dbc(self, value: bool) -> None:
86
+ """ This variable determines is 2D boundary conditions will be automatically assigned based on material properties.
87
+ """
88
+ self._mw_2dbc = value
11
89
 
90
+ @mw_2dbc_lim.setter
91
+ def mw_2dbc_lim(self, value: float):
92
+ """This variable is the bulk conductivity limit in S/m beyond which a surface material will automatically be assigned as a SurfaceImpedance boundary condition."""
93
+ self._mw_2dbc_lim = value
94
+
95
+ @mw_2dbc_peclim.setter
96
+ def mw_2dbc_peclim(self, value: float):
97
+ """This variable determines a bulk conductivity limit in S/m beyond which a conductor is assigned PEC instead of a SurfaceImpedance boundary condition."""
98
+
99
+ self._mw_2dbc_peclim = value
100
+
101
+ @mw_3d_peclim.setter
102
+ def mw_3d_peclim(self, value: float):
103
+ """This variable determines if bulk conductors with a bulk conductivity beyond a limit (.mw_3d_peclim) are considered PEC.
104
+
105
+ """
106
+ self._mw_3d_peclim = value
107
+
108
+ @size_check.setter
109
+ def size_check(self, value: bool):
110
+ """If a total volume check should be considered (100,000 tetrahedra) to hard crash the simulation assuming that the problem size will be too high to solver.
111
+ 100.000 Tetrahedra would yield approximately 700k Degrees of Freedom
112
+ """
113
+ self._size_check = value
114
+
115
+ @mw_cap_sp_single.setter
116
+ def mw_cap_sp_single(self, value: bool) -> bool:
117
+ """If Single S-parameters should be capped with their magnitude to at most 1.0"""
118
+ self._mw_cap_sp_single = value
119
+
120
+ @mw_cap_sp_col.setter
121
+ def mw_cap_sp_col(self, value: bool) -> bool:
122
+ """If Single S-parameters columns should be power normalized to 1.0"""
123
+ self._mw_cap_sp_col = value
124
+
125
+ @mw_recip_sp.setter
126
+ def mw_recip_sp(self, value: bool) -> bool:
127
+ """If reciprodicty should be explicitly enforced"""
128
+ self._mw_recip_sp = value
129
+
12
130
  DEFAULT_SETTINGS = Settings()