emerge 1.0.7__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
 
@@ -341,7 +343,6 @@ class PortMode:
341
343
  norm_factor: float = 1
342
344
  freq: float = 0
343
345
  neff: float = 1
344
- TEM: bool = True
345
346
  Z0: float = 50.0
346
347
  polarity: float = 1.0
347
348
  modetype: Literal['TEM','TE','TM'] = 'TEM'
@@ -459,7 +460,7 @@ class ModalPort(PortBC):
459
460
  port_number: int,
460
461
  cs: CoordinateSystem | None = None,
461
462
  power: float = 1,
462
- TEM: bool = False,
463
+ modetype: Literal['TE','TM','TEM'] | None = None,
463
464
  mixed_materials: bool = False):
464
465
  """Generes a ModalPort boundary condition for a port that requires eigenmode solutions for the mode.
465
466
 
@@ -476,7 +477,7 @@ class ModalPort(PortBC):
476
477
  port_number (int): The port number as an integer
477
478
  cs (CoordinateSystem, optional): The local coordinate system of the port face. Defaults to None.
478
479
  power (float, optional): The radiated power. Defaults to 1.
479
- 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
480
481
  mixed_materials (bool, optional): Wether the port consists of multiple different dielectrics. This requires
481
482
  A recalculation of the port mode at every frequency
482
483
  """
@@ -490,7 +491,7 @@ class ModalPort(PortBC):
490
491
  self.selected_mode: int = 0
491
492
  self.modes: dict[float, list[PortMode]] = defaultdict(list)
492
493
 
493
- self.TEM: bool = TEM
494
+ self.forced_modetype: Literal['TE','TM','TEM'] | None = modetype
494
495
  self.mixed_materials: bool = mixed_materials
495
496
  self.initialized: bool = False
496
497
  self._first_k0: float | None = None
@@ -506,7 +507,25 @@ class ModalPort(PortBC):
506
507
  raise ValueError('No Coordinate System could be derived.')
507
508
  self._er: np.ndarray | None = None
508
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))
509
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
+
510
529
  def portZ0(self, k0: float) -> complex | float | None:
511
530
  return self.get_mode(k0).Z0
512
531
 
@@ -524,6 +543,11 @@ class ModalPort(PortBC):
524
543
  """
525
544
  self.alignment_vectors = [_parse_axis(ax) for ax in axes]
526
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
+
527
551
  def set_terminals(self, positive: Selection | GeoObject | None = None,
528
552
  negative: Selection | GeoObject | None = None,
529
553
  ground: Selection | GeoObject | None = None) -> None:
@@ -616,7 +640,7 @@ class ModalPort(PortBC):
616
640
  beta: float,
617
641
  k0: float,
618
642
  residual: float,
619
- TEM: bool,
643
+ number: int,
620
644
  freq: float) -> PortMode | None:
621
645
  """Add a mode function to the ModalPort
622
646
 
@@ -627,16 +651,17 @@ class ModalPort(PortBC):
627
651
  beta (float): The out-of-plane propagation constant
628
652
  k0 (float): The free space phase constant
629
653
  residual (float): The solution residual
630
- TEM (bool): Whether its a TEM mode
631
654
  freq (float): The frequency of the port mode
632
655
 
633
656
  Returns:
634
657
  PortMode: The port mode object.
635
658
  """
636
- 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
+
637
661
  if mode.energy < 1e-4:
638
662
  logger.debug(f'Ignoring mode due to a low mode energy: {mode.energy}')
639
663
  return None
664
+
640
665
  self.modes[k0].append(mode)
641
666
  self.initialized = True
642
667
 
@@ -659,7 +684,7 @@ class ModalPort(PortBC):
659
684
 
660
685
  def get_beta(self, k0: float) -> float:
661
686
  mode = self.get_mode(k0)
662
- if mode.TEM:
687
+ if self.forced_modetype=='TEM':
663
688
  beta = mode.beta/mode.k0 * k0
664
689
  else:
665
690
  freq = k0*299792458/(2*np.pi)
@@ -816,6 +841,125 @@ class RectangularWaveguide(PortBC):
816
841
  Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
817
842
  return np.array([Exg, Eyg, Ezg])
818
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
+
819
963
  class LumpedPort(PortBC):
820
964
 
821
965
  _include_stiff: bool = True
@@ -874,6 +1018,11 @@ class LumpedPort(PortBC):
874
1018
  self.vintline: list[Line] = []
875
1019
  self.v_integration = True
876
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
+
877
1026
  @property
878
1027
  def surfZ(self) -> float:
879
1028
  """The surface sheet impedance for the lumped port
@@ -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
@@ -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()