emerge 1.0.7__py3-none-any.whl → 1.1.1__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 +15 -3
- emerge/_emerge/const.py +2 -1
- emerge/_emerge/elements/ned2_interp.py +122 -42
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/operations.py +20 -0
- emerge/_emerge/geo/pcb.py +162 -71
- emerge/_emerge/geo/shapes.py +12 -7
- emerge/_emerge/geo/step.py +177 -41
- emerge/_emerge/geometry.py +189 -27
- emerge/_emerge/logsettings.py +26 -2
- emerge/_emerge/material.py +2 -0
- emerge/_emerge/mesh3d.py +6 -8
- emerge/_emerge/mesher.py +67 -11
- emerge/_emerge/mth/common_functions.py +1 -1
- emerge/_emerge/mth/optimized.py +2 -2
- emerge/_emerge/physics/microwave/adaptive_mesh.py +549 -116
- emerge/_emerge/physics/microwave/assembly/assembler.py +9 -1
- emerge/_emerge/physics/microwave/microwave_3d.py +133 -83
- emerge/_emerge/physics/microwave/microwave_bc.py +158 -8
- emerge/_emerge/physics/microwave/microwave_data.py +94 -5
- emerge/_emerge/plot/pyvista/display.py +36 -23
- emerge/_emerge/selection.py +17 -2
- emerge/_emerge/settings.py +124 -6
- emerge/_emerge/simmodel.py +273 -150
- 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 +4 -4
- {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/METADATA +7 -3
- {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/RECORD +33 -32
- {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/WHEEL +0 -0
- {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.7.dist-info → emerge-1.1.1.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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
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)
|
|
@@ -748,6 +773,7 @@ class RectangularWaveguide(PortBC):
|
|
|
748
773
|
logger.info(' - Constructing coordinate system from normal port')
|
|
749
774
|
self.cs = Axis(self.selection.normal).construct_cs()
|
|
750
775
|
logger.debug(f' - Port CS: {self.cs}')
|
|
776
|
+
|
|
751
777
|
def get_basis(self) -> np.ndarray:
|
|
752
778
|
return self.cs._basis
|
|
753
779
|
|
|
@@ -816,6 +842,125 @@ class RectangularWaveguide(PortBC):
|
|
|
816
842
|
Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
|
|
817
843
|
return np.array([Exg, Eyg, Ezg])
|
|
818
844
|
|
|
845
|
+
def _f_zero(k0,x,y,z):
|
|
846
|
+
"Zero field function"
|
|
847
|
+
return np.zeros_like(x, dtype=np.complex128)
|
|
848
|
+
|
|
849
|
+
class UserDefinedPort(PortBC):
|
|
850
|
+
|
|
851
|
+
_include_stiff: bool = True
|
|
852
|
+
_include_mass: bool = False
|
|
853
|
+
_include_force: bool = True
|
|
854
|
+
|
|
855
|
+
def __init__(self,
|
|
856
|
+
face: FaceSelection | GeoSurface,
|
|
857
|
+
port_number: int,
|
|
858
|
+
Ex: Callable | None = None,
|
|
859
|
+
Ey: Callable | None = None,
|
|
860
|
+
Ez: Callable | None = None,
|
|
861
|
+
kz: Callable | None = None,
|
|
862
|
+
power: float = 1.0,
|
|
863
|
+
modetype: Literal['TEM','TE','TM'] = 'TEM',
|
|
864
|
+
cs: CoordinateSystem | None = None):
|
|
865
|
+
"""Creates a user defined port field
|
|
866
|
+
|
|
867
|
+
The UserDefinedPort is defined based on user defined field callables. All undefined callables will default to 0 field or k0.
|
|
868
|
+
|
|
869
|
+
All spatial field functions should be defined using the template:
|
|
870
|
+
>>> def Ec(k0: float, x: np.ndarray, y: np.ndarray, z: np.ndarray) -> np.ndarray
|
|
871
|
+
>>> return #shape like x
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
face (FaceSelection, GeoSurface): The port boundary face selection
|
|
875
|
+
port_number (int): The port number
|
|
876
|
+
Ex (Callable): The Ex(k0,x,y,z) field
|
|
877
|
+
Ey (Callable): The Ey(k0,x,y,z) field
|
|
878
|
+
Ez (Callable): The Ez(k0,x,y,z) field
|
|
879
|
+
kz (Callable): The out of plane propagation constant kz(k0)
|
|
880
|
+
power (float): The port output power
|
|
881
|
+
"""
|
|
882
|
+
super().__init__(face)
|
|
883
|
+
if cs is None:
|
|
884
|
+
cs = GCS
|
|
885
|
+
|
|
886
|
+
self.cs = cs
|
|
887
|
+
self.port_number: int= port_number
|
|
888
|
+
self.active: bool = False
|
|
889
|
+
self.power: float = power
|
|
890
|
+
self.type: str = 'TE'
|
|
891
|
+
if Ex is None:
|
|
892
|
+
Ex = _f_zero
|
|
893
|
+
if Ey is None:
|
|
894
|
+
Ey = _f_zero
|
|
895
|
+
if Ez is None:
|
|
896
|
+
Ez = _f_zero
|
|
897
|
+
if kz is None:
|
|
898
|
+
kz = lambda k0: k0
|
|
899
|
+
|
|
900
|
+
self._fex: Callable = Ex
|
|
901
|
+
self._fey: Callable = Ey
|
|
902
|
+
self._fez: Callable = Ez
|
|
903
|
+
self._fkz: Callable = kz
|
|
904
|
+
self.type = modetype
|
|
905
|
+
|
|
906
|
+
def get_basis(self) -> np.ndarray:
|
|
907
|
+
return self.cs._basis
|
|
908
|
+
|
|
909
|
+
def get_inv_basis(self) -> np.ndarray:
|
|
910
|
+
return self.cs._basis_inv
|
|
911
|
+
|
|
912
|
+
def modetype(self, k0):
|
|
913
|
+
return self.type
|
|
914
|
+
|
|
915
|
+
def get_amplitude(self, k0: float) -> float:
|
|
916
|
+
return np.sqrt(self.power)
|
|
917
|
+
|
|
918
|
+
def get_beta(self, k0: float) -> float:
|
|
919
|
+
''' Return the out of plane propagation constant. βz.'''
|
|
920
|
+
return self._fkz(k0)
|
|
921
|
+
|
|
922
|
+
def get_gamma(self, k0: float) -> complex:
|
|
923
|
+
"""Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
k0 (float): The free space propagation constant.
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
complex: The γ-constant
|
|
930
|
+
"""
|
|
931
|
+
return 1j*self.get_beta(k0)
|
|
932
|
+
|
|
933
|
+
def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0: float) -> np.ndarray:
|
|
934
|
+
return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
|
|
935
|
+
|
|
936
|
+
def port_mode_3d(self,
|
|
937
|
+
x_local: np.ndarray,
|
|
938
|
+
y_local: np.ndarray,
|
|
939
|
+
k0: float,
|
|
940
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
941
|
+
x_global, y_global, z_global = self.cs.in_global_cs(x_local, y_local, 0*x_local)
|
|
942
|
+
|
|
943
|
+
Egxyz = self.port_mode_3d_global(x_global,y_global,z_global,k0,which=which)
|
|
944
|
+
|
|
945
|
+
Ex, Ey, Ez = self.cs.in_local_basis(Egxyz[0,:], Egxyz[1,:], Egxyz[2,:])
|
|
946
|
+
|
|
947
|
+
Exyz = np.array([Ex, Ey, Ez])
|
|
948
|
+
return Exyz
|
|
949
|
+
|
|
950
|
+
def port_mode_3d_global(self,
|
|
951
|
+
x_global: np.ndarray,
|
|
952
|
+
y_global: np.ndarray,
|
|
953
|
+
z_global: np.ndarray,
|
|
954
|
+
k0: float,
|
|
955
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
956
|
+
'''Compute the port mode field for global xyz coordinates.'''
|
|
957
|
+
xl, yl, _ = self.cs.in_local_cs(x_global, y_global, z_global)
|
|
958
|
+
Ex = self._fex(k0, x_global, y_global, z_global)
|
|
959
|
+
Ey = self._fey(k0, x_global, y_global, z_global)
|
|
960
|
+
Ez = self._fez(k0, x_global, y_global, z_global)
|
|
961
|
+
Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
|
|
962
|
+
return np.array([Exg, Eyg, Ezg])
|
|
963
|
+
|
|
819
964
|
class LumpedPort(PortBC):
|
|
820
965
|
|
|
821
966
|
_include_stiff: bool = True
|
|
@@ -874,6 +1019,11 @@ class LumpedPort(PortBC):
|
|
|
874
1019
|
self.vintline: list[Line] = []
|
|
875
1020
|
self.v_integration = True
|
|
876
1021
|
|
|
1022
|
+
# Sanity checks
|
|
1023
|
+
if self.width > 0.5 or self.height > 0.5:
|
|
1024
|
+
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')
|
|
1025
|
+
|
|
1026
|
+
|
|
877
1027
|
@property
|
|
878
1028
|
def surfZ(self) -> float:
|
|
879
1029
|
"""The surface sheet impedance for the lumped port
|
|
@@ -667,13 +667,51 @@ class MWField:
|
|
|
667
667
|
return sum([self.excitation[mode.port_number]*self._fields[mode.port_number] for mode in self.port_modes]) # type: ignore
|
|
668
668
|
|
|
669
669
|
def set_field_vector(self) -> None:
|
|
670
|
-
"""Defines the default excitation coefficients for the current dataset"""
|
|
670
|
+
"""Defines the default excitation coefficients for the current dataset as an excitation of only port 1."""
|
|
671
671
|
self.excitation = {key: 0.0 for key in self._fields.keys()}
|
|
672
672
|
self.excitation[self.port_modes[0].port_number] = 1.0 + 0j
|
|
673
673
|
|
|
674
|
-
def excite_port(self, number: int) -> None:
|
|
674
|
+
def excite_port(self, number: int, excitation: complex = 1.0 + 0.0j) -> None:
|
|
675
|
+
"""Excite a single port provided by a given port number
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
number (int): The port number to excite
|
|
679
|
+
coefficient (complex): The port excitation. Defaults to 1.0 + 0.0j
|
|
680
|
+
"""
|
|
681
|
+
self.excitation = {key: 0.0 for key in self._fields.keys()}
|
|
682
|
+
self.excitation[self.port_modes[number-1].port_number] = excitation
|
|
683
|
+
|
|
684
|
+
def set_excitations(self, *excitations: complex) -> None:
|
|
685
|
+
"""Set bulk port excitations by an ordered array of excitation coefficients.
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
*complex: A sequence of complex numbers
|
|
689
|
+
"""
|
|
675
690
|
self.excitation = {key: 0.0 for key in self._fields.keys()}
|
|
676
|
-
|
|
691
|
+
for iport, coeff in enumerate(excitations):
|
|
692
|
+
self.excitation[self.port_modes[iport].port_number] = coeff
|
|
693
|
+
|
|
694
|
+
def combine_ports(self, p1: int, p2: int) -> MWField:
|
|
695
|
+
"""Combines ports p1 and p2 into a cifferential and common mode port respectively.
|
|
696
|
+
|
|
697
|
+
The p1 index becomes the differential mode port
|
|
698
|
+
The p2 index becomes the common mode port
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
p1 (int): The first port number
|
|
702
|
+
p2 (int): The second port number
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
MWField: _description_
|
|
706
|
+
"""
|
|
707
|
+
|
|
708
|
+
fp1 = self._fields[p1]
|
|
709
|
+
fp2 = self._fields[p2]
|
|
710
|
+
|
|
711
|
+
self._fields[p1] = (fp1-fp2)/np.sqrt(2)
|
|
712
|
+
self._fields[p2] = (fp1+fp2)/np.sqrt(2)
|
|
713
|
+
|
|
714
|
+
return self
|
|
677
715
|
|
|
678
716
|
@property
|
|
679
717
|
def EH(self) -> tuple[np.ndarray, np.ndarray]:
|
|
@@ -720,7 +758,9 @@ class MWField:
|
|
|
720
758
|
xf = xs.flatten()
|
|
721
759
|
yf = ys.flatten()
|
|
722
760
|
zf = zs.flatten()
|
|
761
|
+
logger.debug(f'Interpolating {xf.shape[0]} field points')
|
|
723
762
|
Ex, Ey, Ez = self.basis.interpolate(self._field, xf, yf, zf, usenan=usenan)
|
|
763
|
+
logger.debug('E Interpolation complete')
|
|
724
764
|
self.Ex = Ex.reshape(shp)
|
|
725
765
|
self.Ey = Ey.reshape(shp)
|
|
726
766
|
self.Ez = Ez.reshape(shp)
|
|
@@ -728,6 +768,7 @@ class MWField:
|
|
|
728
768
|
|
|
729
769
|
constants = 1/ (-1j*2*np.pi*self.freq*(self._dur*MU0) )
|
|
730
770
|
Hx, Hy, Hz = self.basis.interpolate_curl(self._field, xf, yf, zf, constants, usenan=usenan)
|
|
771
|
+
logger.debug('H Interpolation complete')
|
|
731
772
|
ids = self.basis.interpolate_index(xf, yf, zf)
|
|
732
773
|
|
|
733
774
|
self.er = self._der[ids].reshape(shp)
|
|
@@ -743,10 +784,10 @@ class MWField:
|
|
|
743
784
|
|
|
744
785
|
return field
|
|
745
786
|
|
|
746
|
-
def _solution_quality(self) -> tuple[np.ndarray, np.ndarray]:
|
|
787
|
+
def _solution_quality(self, solve_ids: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
747
788
|
from .adaptive_mesh import compute_error_estimate
|
|
748
789
|
|
|
749
|
-
error_tet, max_elem_size = compute_error_estimate(self)
|
|
790
|
+
error_tet, max_elem_size = compute_error_estimate(self, solve_ids)
|
|
750
791
|
return error_tet, max_elem_size
|
|
751
792
|
|
|
752
793
|
def boundary(self,
|
|
@@ -1146,6 +1187,54 @@ class MWScalarNdim:
|
|
|
1146
1187
|
def S(self, i1: int, i2: int) -> np.ndarray:
|
|
1147
1188
|
return self.Sp[...,self._portmap[i1], self._portmap[i2]]
|
|
1148
1189
|
|
|
1190
|
+
def combine_ports(self, p1: int, p2: int) -> MWScalarNdim:
|
|
1191
|
+
"""Combine ports p1 and p2 into a differential and common mode port respectively.
|
|
1192
|
+
|
|
1193
|
+
The p1 index becomes the differential mode port
|
|
1194
|
+
The p2 index becomes the common mode port
|
|
1195
|
+
|
|
1196
|
+
Args:
|
|
1197
|
+
p1 (int): The first port number
|
|
1198
|
+
p2 (int): The second port number
|
|
1199
|
+
|
|
1200
|
+
Returns:
|
|
1201
|
+
MWScalarNdim: _description_
|
|
1202
|
+
"""
|
|
1203
|
+
if p1==p2:
|
|
1204
|
+
raise ValueError('p1 and p2 must be different port numbers')
|
|
1205
|
+
|
|
1206
|
+
F, N, _ = self.Sp.shape
|
|
1207
|
+
p1 = p1-1
|
|
1208
|
+
p2 = p2-1
|
|
1209
|
+
|
|
1210
|
+
if not (0 <= p1 < N and 0 <= p2 < N):
|
|
1211
|
+
raise IndexError(f'Ports {p1+1} or {p2+1} are out of range {N}')
|
|
1212
|
+
|
|
1213
|
+
Sout = self.Sp.copy()
|
|
1214
|
+
ii, jj = p1, p2
|
|
1215
|
+
idx = np.ones(N, dtype=np.bool)
|
|
1216
|
+
idx[[ii,jj]] = False
|
|
1217
|
+
others = np.nonzero(idx)[0]
|
|
1218
|
+
isqrt2 = 1.0 / np.sqrt(2.0)
|
|
1219
|
+
|
|
1220
|
+
Sout[:, others, ii] = (self.Sp[:, others, ii] - self.Sp[:, others, jj]) * isqrt2
|
|
1221
|
+
Sout[:, others, jj] = (self.Sp[:, others, ii] + self.Sp[:, others, jj]) * isqrt2
|
|
1222
|
+
Sout[:, ii, others] = (self.Sp[:, ii, others] - self.Sp[:, jj, others]) * isqrt2
|
|
1223
|
+
Sout[:, jj, others] = (self.Sp[:, ii, others] + self.Sp[:, jj, others]) * isqrt2
|
|
1224
|
+
|
|
1225
|
+
Sii = self.Sp[:, ii, ii]
|
|
1226
|
+
Sij = self.Sp[:, ii, jj]
|
|
1227
|
+
Sji = self.Sp[:, jj, ii]
|
|
1228
|
+
Sjj = self.Sp[:, jj, jj]
|
|
1229
|
+
|
|
1230
|
+
Sout[:, ii, ii] = 0.5 *(Sii - Sij - Sji + Sjj)
|
|
1231
|
+
Sout[:, ii, jj] = 0.5 *(Sii + Sij - Sji - Sjj)
|
|
1232
|
+
Sout[:, jj, ii] = 0.5 *(Sii - Sij + Sji - Sjj)
|
|
1233
|
+
Sout[:, jj, jj] = 0.5 *(Sii + Sij + Sji + Sjj)
|
|
1234
|
+
|
|
1235
|
+
self.Sp = Sout
|
|
1236
|
+
|
|
1237
|
+
return self
|
|
1149
1238
|
@property
|
|
1150
1239
|
def Smat(self) -> np.ndarray:
|
|
1151
1240
|
"""Returns the full S-matrix
|
|
@@ -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
|
|
@@ -241,6 +242,7 @@ class PVDisplay(BaseDisplay):
|
|
|
241
242
|
self._stop: bool = False
|
|
242
243
|
self._objs: list[_AnimObject] = []
|
|
243
244
|
self._do_animate: bool = False
|
|
245
|
+
self._animate_next: bool = False
|
|
244
246
|
self._closed_via_x: bool = False
|
|
245
247
|
self._Nsteps: int = 0
|
|
246
248
|
self._fps: int = 25
|
|
@@ -260,6 +262,9 @@ class PVDisplay(BaseDisplay):
|
|
|
260
262
|
self._cbar_lim: tuple[float, float] | None = None
|
|
261
263
|
self.camera_position = (1, -1, 1) # +X, +Z, -Y
|
|
262
264
|
|
|
265
|
+
@property
|
|
266
|
+
def _mesh(self) -> Mesh3D:
|
|
267
|
+
return self._state.mesh
|
|
263
268
|
|
|
264
269
|
def cbar(self, name: str, n_labels: int = 5, interactive: bool = False, clim: tuple[float, float] | None = None ) -> PVDisplay:
|
|
265
270
|
self._cbar_args = dict(title=name, n_labels=n_labels, interactive=interactive)
|
|
@@ -302,8 +307,7 @@ class PVDisplay(BaseDisplay):
|
|
|
302
307
|
self._ruler.min_length = max(1e-3, min(self._mesh.edge_lengths))
|
|
303
308
|
self._update_camera()
|
|
304
309
|
self._add_aux_items()
|
|
305
|
-
|
|
306
|
-
# self._plot.enable_anti_aliasing(self.set.anti_aliassing)
|
|
310
|
+
self._add_background()
|
|
307
311
|
if self._do_animate:
|
|
308
312
|
self._wire_close_events()
|
|
309
313
|
self.add_text('Press Q to close!',color='red', position='upper_left')
|
|
@@ -313,14 +317,17 @@ class PVDisplay(BaseDisplay):
|
|
|
313
317
|
self._plot.show()
|
|
314
318
|
|
|
315
319
|
self._reset()
|
|
316
|
-
|
|
317
|
-
def set_mesh(self, mesh: Mesh3D):
|
|
318
|
-
"""Define the mesh to be used
|
|
319
320
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
def _add_background(self):
|
|
322
|
+
from pyvista import examples
|
|
323
|
+
from requests.exceptions import ConnectionError
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
cubemap = examples.download_sky_box_cube_map()
|
|
327
|
+
self._plot.set_environment_texture(cubemap)
|
|
328
|
+
except ConnectionError:
|
|
329
|
+
logger.warning(f'No internet, no background texture will be used.')
|
|
330
|
+
|
|
324
331
|
|
|
325
332
|
def _reset(self):
|
|
326
333
|
""" Resets key display parameters."""
|
|
@@ -328,6 +335,7 @@ class PVDisplay(BaseDisplay):
|
|
|
328
335
|
self._plot = pv.Plotter()
|
|
329
336
|
self._stop = False
|
|
330
337
|
self._objs = []
|
|
338
|
+
self._animate_next = False
|
|
331
339
|
C_CYCLE = _gen_c_cycle()
|
|
332
340
|
|
|
333
341
|
def _close_callback(self, arg):
|
|
@@ -397,6 +405,7 @@ class PVDisplay(BaseDisplay):
|
|
|
397
405
|
print('If you closed the animation without using (Q) press Ctrl+C to kill the process.')
|
|
398
406
|
self._Nsteps = Nsteps
|
|
399
407
|
self._fps = fps
|
|
408
|
+
self._animate_next = True
|
|
400
409
|
self._do_animate = True
|
|
401
410
|
return self
|
|
402
411
|
|
|
@@ -456,6 +465,10 @@ class PVDisplay(BaseDisplay):
|
|
|
456
465
|
## OBLIGATORY METHODS
|
|
457
466
|
def add_object(self, obj: GeoObject | Selection, mesh: bool = False, volume_mesh: bool = True, label: bool = False, *args, **kwargs):
|
|
458
467
|
|
|
468
|
+
if isinstance(obj, GeoObject):
|
|
469
|
+
if obj._hidden:
|
|
470
|
+
return
|
|
471
|
+
|
|
459
472
|
show_edges = False
|
|
460
473
|
opacity = obj.opacity
|
|
461
474
|
line_width = 0.5
|
|
@@ -508,7 +521,7 @@ class PVDisplay(BaseDisplay):
|
|
|
508
521
|
|
|
509
522
|
self._plot.add_mesh(self._volume_edges(_select(obj)), color='#000000', line_width=2, show_edges=True)
|
|
510
523
|
|
|
511
|
-
if
|
|
524
|
+
if label:
|
|
512
525
|
points = []
|
|
513
526
|
labels = []
|
|
514
527
|
for dt in obj.dimtags:
|
|
@@ -681,14 +694,14 @@ class PVDisplay(BaseDisplay):
|
|
|
681
694
|
actor = self._plot.add_mesh(grid_no_nan, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
|
|
682
695
|
|
|
683
696
|
|
|
684
|
-
if self.
|
|
697
|
+
if self._animate_next:
|
|
685
698
|
def on_update(obj: _AnimObject, phi: complex):
|
|
686
699
|
field_anim = obj.T(np.real(obj.field * phi))
|
|
687
700
|
obj.grid[name] = field_anim
|
|
688
701
|
obj.fgrid[name] = obj.grid.threshold(scalars=name)[name]
|
|
689
702
|
#obj.fgrid replace with thresholded scalar data.
|
|
690
703
|
self._objs.append(_AnimObject(field_flat, T, grid, grid_no_nan, actor, on_update))
|
|
691
|
-
|
|
704
|
+
self._animate_next = False
|
|
692
705
|
self._reset_cbar()
|
|
693
706
|
|
|
694
707
|
def add_boundary_field(self,
|
|
@@ -763,13 +776,13 @@ class PVDisplay(BaseDisplay):
|
|
|
763
776
|
kwargs = setdefault(kwargs, cmap=cmap, clim=clim, opacity=opacity, pickable=False, multi_colors=True)
|
|
764
777
|
actor = self._plot.add_mesh(grid, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
|
|
765
778
|
|
|
766
|
-
if self.
|
|
779
|
+
if self._animate_next:
|
|
767
780
|
def on_update(obj: _AnimObject, phi: complex):
|
|
768
781
|
field_anim = obj.T(np.real(obj.field * phi))
|
|
769
782
|
obj.grid[name] = field_anim
|
|
770
783
|
#obj.fgrid replace with thresholded scalar data.
|
|
771
784
|
self._objs.append(_AnimObject(field_flat, T, grid, grid, actor, on_update))
|
|
772
|
-
|
|
785
|
+
self._animate_next = False
|
|
773
786
|
self._reset_cbar()
|
|
774
787
|
|
|
775
788
|
def add_title(self, title: str) -> None:
|
|
@@ -911,7 +924,7 @@ class PVDisplay(BaseDisplay):
|
|
|
911
924
|
|
|
912
925
|
actor = self._plot.add_mesh(contour, opacity=opacity, cmap=cmap, clim=clim, pickable=False, scalar_bar_args=self._cbar_args)
|
|
913
926
|
|
|
914
|
-
if self.
|
|
927
|
+
if self._animate_next:
|
|
915
928
|
def on_update(obj: _AnimObject, phi: complex):
|
|
916
929
|
new_vals = obj.T(np.real(obj.field * phi))
|
|
917
930
|
obj.grid['anim'] = new_vals
|
|
@@ -919,7 +932,7 @@ class PVDisplay(BaseDisplay):
|
|
|
919
932
|
obj.actor.GetMapper().SetInputData(new_contour) # type: ignore
|
|
920
933
|
|
|
921
934
|
self._objs.append(_AnimObject(field, T, grid, None, actor, on_update)) # type: ignore
|
|
922
|
-
|
|
935
|
+
self._animate_next = False
|
|
923
936
|
self._reset_cbar()
|
|
924
937
|
|
|
925
938
|
def _add_aux_items(self) -> None:
|
emerge/_emerge/selection.py
CHANGED
|
@@ -21,6 +21,9 @@ import numpy as np
|
|
|
21
21
|
from .cs import Axis, CoordinateSystem, _parse_vector, Plane
|
|
22
22
|
from typing import Callable, TypeVar, Iterable, Any
|
|
23
23
|
|
|
24
|
+
class SelectionError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
24
27
|
def align_rectangle_frame(pts3d: np.ndarray, normal: np.ndarray) -> dict[str, Any]:
|
|
25
28
|
"""Tries to find a rectangle as convex-hull of a set of points with a given normal vector.
|
|
26
29
|
|
|
@@ -174,7 +177,7 @@ class Selection:
|
|
|
174
177
|
"""
|
|
175
178
|
dim: int = -1
|
|
176
179
|
def __init__(self, tags: list[int] | set[int] | tuple[int] | None = None):
|
|
177
|
-
|
|
180
|
+
self.name: str = 'Selection'
|
|
178
181
|
self._tags: set[int] = set()
|
|
179
182
|
|
|
180
183
|
if tags is not None:
|
|
@@ -252,7 +255,19 @@ class Selection:
|
|
|
252
255
|
maxy = max(maxy, y1)
|
|
253
256
|
maxz = max(maxz, z1)
|
|
254
257
|
return (minx, miny, minz), (maxx, maxy, maxz)
|
|
255
|
-
|
|
258
|
+
|
|
259
|
+
def _named(self, name: str) -> Selection:
|
|
260
|
+
"""Sets the name of the selection and returns it
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
name (str): The name of the selection
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Selection: The same selection object
|
|
267
|
+
"""
|
|
268
|
+
self.name = name
|
|
269
|
+
return self
|
|
270
|
+
|
|
256
271
|
def exclude(self, xyz_excl_function: Callable = lambda x,y,z: True, plane: Plane | None = None, axis: Axis | None = None) -> Selection:
|
|
257
272
|
"""Exclude points by evaluating a function(x,y,z)-> bool
|
|
258
273
|
|