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.
- emerge/__init__.py +14 -3
- emerge/_emerge/elements/index_interp.py +45 -0
- emerge/_emerge/geo/pcb.py +132 -59
- emerge/_emerge/geo/shapes.py +11 -7
- emerge/_emerge/geometry.py +23 -8
- emerge/_emerge/logsettings.py +26 -2
- emerge/_emerge/material.py +1 -1
- emerge/_emerge/mesh3d.py +38 -8
- emerge/_emerge/mesher.py +61 -11
- emerge/_emerge/physics/microwave/adaptive_mesh.py +667 -0
- emerge/_emerge/physics/microwave/assembly/assembler.py +6 -5
- emerge/_emerge/physics/microwave/microwave_3d.py +251 -88
- emerge/_emerge/physics/microwave/microwave_bc.py +182 -11
- emerge/_emerge/physics/microwave/microwave_data.py +23 -6
- emerge/_emerge/plot/pyvista/display.py +31 -18
- emerge/_emerge/settings.py +124 -6
- emerge/_emerge/simmodel.py +238 -95
- emerge/_emerge/simstate.py +106 -0
- emerge/_emerge/simulation_data.py +11 -23
- emerge/_emerge/solve_interfaces/cudss_interface.py +20 -1
- emerge/_emerge/solver.py +2 -2
- {emerge-1.0.6.dist-info → emerge-1.1.0.dist-info}/METADATA +9 -4
- {emerge-1.0.6.dist-info → emerge-1.1.0.dist-info}/RECORD +26 -24
- {emerge-1.0.6.dist-info → emerge-1.1.0.dist-info}/WHEEL +0 -0
- {emerge-1.0.6.dist-info → emerge-1.1.0.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.6.dist-info → emerge-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
236
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
emerge/_emerge/settings.py
CHANGED
|
@@ -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.
|
|
8
|
-
self.
|
|
9
|
-
self.
|
|
10
|
-
self.
|
|
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()
|