emerge 0.6.7__py3-none-any.whl → 0.6.9__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 +2 -2
- emerge/_emerge/_cache_check.py +1 -1
- emerge/_emerge/elements/femdata.py +3 -2
- emerge/_emerge/elements/index_interp.py +1 -2
- emerge/_emerge/elements/ned2_interp.py +16 -16
- emerge/_emerge/elements/nedelec2.py +17 -6
- emerge/_emerge/elements/nedleg2.py +21 -9
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/horn.py +0 -1
- emerge/_emerge/geo/modeler.py +1 -1
- emerge/_emerge/geo/operations.py +13 -0
- emerge/_emerge/geo/pcb_tools/calculator.py +2 -3
- emerge/_emerge/geo/pmlbox.py +35 -11
- emerge/_emerge/geometry.py +13 -6
- emerge/_emerge/material.py +334 -82
- emerge/_emerge/mesh3d.py +14 -8
- emerge/_emerge/physics/microwave/assembly/assembler.py +43 -20
- emerge/_emerge/physics/microwave/microwave_3d.py +57 -44
- emerge/_emerge/physics/microwave/microwave_bc.py +26 -24
- emerge/_emerge/physics/microwave/microwave_data.py +90 -7
- emerge/_emerge/plot/pyvista/display.py +53 -15
- emerge/_emerge/plot/pyvista/display_settings.py +4 -1
- emerge/_emerge/plot/simple_plots.py +42 -26
- emerge/_emerge/projects/_load_base.txt +1 -2
- emerge/_emerge/selection.py +4 -0
- emerge/_emerge/simmodel.py +21 -9
- emerge/_emerge/solver.py +45 -18
- emerge/lib.py +256 -250
- {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/METADATA +2 -1
- {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/RECORD +33 -33
- {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/licenses/LICENSE +2 -2
- {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/WHEEL +0 -0
- {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/entry_points.txt +0 -0
|
@@ -269,7 +269,7 @@ class Microwave3D:
|
|
|
269
269
|
Callable: The discretizer function
|
|
270
270
|
"""
|
|
271
271
|
def disc(material: Material):
|
|
272
|
-
return 299792458/(max(self.frequencies) * np.real(material.neff))
|
|
272
|
+
return 299792458/(max(self.frequencies) * np.real(material.neff(max(self.frequencies))))
|
|
273
273
|
return disc
|
|
274
274
|
|
|
275
275
|
def _initialize_field(self):
|
|
@@ -467,10 +467,25 @@ class Microwave3D:
|
|
|
467
467
|
raise SimulationError('Cannot proceed, the current basis class is undefined.')
|
|
468
468
|
|
|
469
469
|
logger.debug('Retreiving material properties.')
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
470
|
+
|
|
471
|
+
if freq is None:
|
|
472
|
+
freq = self.frequencies[0]
|
|
473
|
+
|
|
474
|
+
materials = self.mesh.retreive(self.mesher.volumes)
|
|
473
475
|
|
|
476
|
+
ertet = np.zeros((3,3,self.mesh.n_tets), dtype=np.complex128)
|
|
477
|
+
tandtet = np.zeros((3,3,self.mesh.n_tets), dtype=np.complex128)
|
|
478
|
+
urtet = np.zeros((3,3,self.mesh.n_tets), dtype=np.complex128)
|
|
479
|
+
condtet = np.zeros((3,3,self.mesh.n_tets), dtype=np.complex128)
|
|
480
|
+
|
|
481
|
+
for mat in materials:
|
|
482
|
+
ertet = mat.er(freq, ertet)
|
|
483
|
+
tandtet = mat.tand(freq, tandtet)
|
|
484
|
+
urtet = mat.ur(freq, urtet)
|
|
485
|
+
condtet = mat.cond(freq, condtet)
|
|
486
|
+
|
|
487
|
+
ertet = ertet * (1-1j*tandtet)
|
|
488
|
+
|
|
474
489
|
er = np.zeros((3,3,self.mesh.n_tris,), dtype=np.complex128)
|
|
475
490
|
ur = np.zeros((3,3,self.mesh.n_tris,), dtype=np.complex128)
|
|
476
491
|
cond = np.zeros((self.mesh.n_tris,), dtype=np.complex128)
|
|
@@ -479,7 +494,7 @@ class Microwave3D:
|
|
|
479
494
|
itet = self.mesh.tri_to_tet[0,itri]
|
|
480
495
|
er[:,:,itri] = ertet[:,:,itet]
|
|
481
496
|
ur[:,:,itri] = urtet[:,:,itet]
|
|
482
|
-
cond[itri] = condtet[itet]
|
|
497
|
+
cond[itri] = condtet[0,0,itet]
|
|
483
498
|
|
|
484
499
|
itri_port = self.mesh.get_triangles(port.tags)
|
|
485
500
|
|
|
@@ -488,11 +503,7 @@ class Microwave3D:
|
|
|
488
503
|
ermax = np.max(er[:,:,itri_port].flatten())
|
|
489
504
|
urmax = np.max(ur[:,:,itri_port].flatten())
|
|
490
505
|
|
|
491
|
-
if freq is None:
|
|
492
|
-
freq = self.frequencies[0]
|
|
493
|
-
|
|
494
506
|
k0 = 2*np.pi*freq/299792458
|
|
495
|
-
kmax = k0*np.sqrt(ermax.real*urmax.real)
|
|
496
507
|
|
|
497
508
|
Amatrix, Bmatrix, solve_ids, nlf = self.assembler.assemble_bma_matrices(self.basis, er, ur, cond, k0, port, self.bc)
|
|
498
509
|
|
|
@@ -511,8 +522,7 @@ class Microwave3D:
|
|
|
511
522
|
else:
|
|
512
523
|
|
|
513
524
|
target_kz = ermean*urmean*0.7*k0
|
|
514
|
-
|
|
515
|
-
|
|
525
|
+
|
|
516
526
|
logger.debug(f'Solving for {solve_ids.shape[0]} degrees of freedom.')
|
|
517
527
|
|
|
518
528
|
eigen_values, eigen_modes, report = self.solveroutine.eig_boundary(Amatrix, Bmatrix, solve_ids, nmodes, direct, target_kz, sign=-1)
|
|
@@ -550,12 +560,10 @@ class Microwave3D:
|
|
|
550
560
|
Ez = np.max(np.abs(Efz))
|
|
551
561
|
Exy = np.max(np.abs(Efxy))
|
|
552
562
|
|
|
553
|
-
|
|
554
|
-
# Ez = 0
|
|
555
|
-
if Ez/Exy < 1e-3 and not TEM:
|
|
563
|
+
if Ez/Exy < 1e-1 and not TEM:
|
|
556
564
|
logger.debug('Low Ez/Et ratio detected, assuming TE mode')
|
|
557
565
|
mode.modetype = 'TE'
|
|
558
|
-
elif Ez/Exy > 1e-
|
|
566
|
+
elif Ez/Exy > 1e-1 and not TEM:
|
|
559
567
|
logger.debug('High Ez/Et ratio detected, assuming TM mode')
|
|
560
568
|
mode.modetype = 'TM'
|
|
561
569
|
elif TEM:
|
|
@@ -621,9 +629,7 @@ class Microwave3D:
|
|
|
621
629
|
if self.basis is None:
|
|
622
630
|
raise SimulationError('Cannot proceed, the simulation basis class is undefined.')
|
|
623
631
|
|
|
624
|
-
|
|
625
|
-
ur = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
|
|
626
|
-
cond = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
|
|
632
|
+
materials = self.mesh.retreive(self.mesher.volumes)
|
|
627
633
|
|
|
628
634
|
### Does this move
|
|
629
635
|
logger.debug('Initializing frequency domain sweep.')
|
|
@@ -674,7 +680,8 @@ class Microwave3D:
|
|
|
674
680
|
freq_groups = [self.frequencies[i:i+n] for i in range(0, len(self.frequencies), n)]
|
|
675
681
|
|
|
676
682
|
results: list[SimJob] = []
|
|
677
|
-
|
|
683
|
+
matset: list[tuple[np.ndarray, np.ndarray, np.ndarray]] = []
|
|
684
|
+
|
|
678
685
|
## Single threaded
|
|
679
686
|
job_id = 1
|
|
680
687
|
|
|
@@ -691,7 +698,7 @@ class Microwave3D:
|
|
|
691
698
|
logger.debug(f'Simulation frequency = {freq/1e9:.3f} GHz')
|
|
692
699
|
if automatic_modal_analysis:
|
|
693
700
|
self._compute_modes(freq)
|
|
694
|
-
job = self.assembler.assemble_freq_matrix(self.basis,
|
|
701
|
+
job, mats = self.assembler.assemble_freq_matrix(self.basis, materials,
|
|
695
702
|
self.bc.boundary_conditions,
|
|
696
703
|
freq,
|
|
697
704
|
cache_matrices=self.cache_matrices)
|
|
@@ -700,6 +707,7 @@ class Microwave3D:
|
|
|
700
707
|
job.id = job_id
|
|
701
708
|
job_id += 1
|
|
702
709
|
jobs.append(job)
|
|
710
|
+
matset.append(mats)
|
|
703
711
|
|
|
704
712
|
logger.info(f'Starting single threaded solve of {len(jobs)} jobs.')
|
|
705
713
|
group_results = [run_job_single(job) for job in jobs]
|
|
@@ -716,7 +724,7 @@ class Microwave3D:
|
|
|
716
724
|
logger.debug(f'Simulation frequency = {freq/1e9:.3f} GHz')
|
|
717
725
|
if automatic_modal_analysis:
|
|
718
726
|
self._compute_modes(freq)
|
|
719
|
-
job = self.assembler.assemble_freq_matrix(self.basis,
|
|
727
|
+
job, mats = self.assembler.assemble_freq_matrix(self.basis, materials,
|
|
720
728
|
self.bc.boundary_conditions,
|
|
721
729
|
freq,
|
|
722
730
|
cache_matrices=self.cache_matrices)
|
|
@@ -725,6 +733,7 @@ class Microwave3D:
|
|
|
725
733
|
job.id = job_id
|
|
726
734
|
job_id += 1
|
|
727
735
|
jobs.append(job)
|
|
736
|
+
matset.append(mats)
|
|
728
737
|
|
|
729
738
|
logger.info(f'Starting distributed solve of {len(jobs)} jobs with {njobs} threads.')
|
|
730
739
|
group_results = list(executor.map(run_job, jobs))
|
|
@@ -749,8 +758,8 @@ class Microwave3D:
|
|
|
749
758
|
if automatic_modal_analysis:
|
|
750
759
|
self._compute_modes(freq)
|
|
751
760
|
|
|
752
|
-
job = self.assembler.assemble_freq_matrix(
|
|
753
|
-
self.basis,
|
|
761
|
+
job, mats = self.assembler.assemble_freq_matrix(
|
|
762
|
+
self.basis, materials,
|
|
754
763
|
self.bc.boundary_conditions,
|
|
755
764
|
freq,
|
|
756
765
|
cache_matrices=self.cache_matrices
|
|
@@ -761,6 +770,7 @@ class Microwave3D:
|
|
|
761
770
|
job.id = job_id
|
|
762
771
|
job_id += 1
|
|
763
772
|
jobs.append(job)
|
|
773
|
+
matset.append(mats)
|
|
764
774
|
|
|
765
775
|
logger.info(
|
|
766
776
|
f'Starting distributed solve of {len(jobs)} jobs '
|
|
@@ -783,7 +793,7 @@ class Microwave3D:
|
|
|
783
793
|
|
|
784
794
|
self.solveroutine.reset()
|
|
785
795
|
### Compute S-parameters and return
|
|
786
|
-
self._post_process(results,
|
|
796
|
+
self._post_process(results, matset)
|
|
787
797
|
return self.data
|
|
788
798
|
|
|
789
799
|
def eigenmode(self, search_frequency: float,
|
|
@@ -817,19 +827,21 @@ class Microwave3D:
|
|
|
817
827
|
if self.basis is None:
|
|
818
828
|
raise SimulationError('Cannot proceed. The simulation basis class is undefined.')
|
|
819
829
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
830
|
+
materials = self.mesh.retreive(self.mesher.volumes)
|
|
831
|
+
|
|
832
|
+
# er = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
|
|
833
|
+
# ur = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
|
|
834
|
+
# cond = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
|
|
823
835
|
|
|
824
836
|
### Does this move
|
|
825
837
|
logger.debug('Initializing frequency domain sweep.')
|
|
826
838
|
|
|
827
839
|
logger.info(f'Pre-assembling matrices of {len(self.frequencies)} frequency points.')
|
|
828
840
|
|
|
829
|
-
job = self.assembler.assemble_eig_matrix(self.basis,
|
|
841
|
+
job, matset = self.assembler.assemble_eig_matrix(self.basis, materials,
|
|
830
842
|
self.bc.boundary_conditions, search_frequency)
|
|
831
843
|
|
|
832
|
-
|
|
844
|
+
er, ur, cond = matset
|
|
833
845
|
logger.info('Solving complete')
|
|
834
846
|
|
|
835
847
|
A, C, solve_ids = job.yield_AC()
|
|
@@ -846,7 +858,7 @@ class Microwave3D:
|
|
|
846
858
|
|
|
847
859
|
for i in range(nmodes_found):
|
|
848
860
|
|
|
849
|
-
|
|
861
|
+
|
|
850
862
|
eig_k0 = np.sqrt(eigen_values[i])
|
|
851
863
|
if eig_k0 < k0_limit:
|
|
852
864
|
logger.debug(f'Ignoring mode due to low k0: {eig_k0} < {k0_limit}')
|
|
@@ -856,11 +868,11 @@ class Microwave3D:
|
|
|
856
868
|
logger.debug(f'Found k0={eig_k0:.2f}, f0={eig_freq/1e9:.2f} GHz')
|
|
857
869
|
Emode = eigen_modes[:,i]
|
|
858
870
|
|
|
859
|
-
scalardata = self.data.scalar.new(
|
|
871
|
+
scalardata = self.data.scalar.new(**self._params)
|
|
860
872
|
scalardata.k0 = eig_k0
|
|
861
873
|
scalardata.freq = eig_freq
|
|
862
874
|
|
|
863
|
-
fielddata = self.data.field.new(
|
|
875
|
+
fielddata = self.data.field.new(**self._params)
|
|
864
876
|
fielddata.freq = eig_freq
|
|
865
877
|
fielddata._der = np.squeeze(er[0,0,:])
|
|
866
878
|
fielddata._dur = np.squeeze(ur[0,0,:])
|
|
@@ -870,7 +882,7 @@ class Microwave3D:
|
|
|
870
882
|
|
|
871
883
|
return self.data
|
|
872
884
|
|
|
873
|
-
def _post_process(self, results: list[SimJob],
|
|
885
|
+
def _post_process(self, results: list[SimJob], materials: list[tuple[np.ndarray, np.ndarray, np.ndarray]]):
|
|
874
886
|
"""Compute the S-parameters after Frequency sweep
|
|
875
887
|
|
|
876
888
|
Args:
|
|
@@ -888,18 +900,19 @@ class Microwave3D:
|
|
|
888
900
|
|
|
889
901
|
logger.info('Computing S-parameters')
|
|
890
902
|
|
|
891
|
-
ertri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
|
|
892
|
-
urtri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
|
|
893
|
-
condtri = np.zeros((self.mesh.n_tris,), dtype=np.complex128)
|
|
894
|
-
|
|
895
|
-
for itri in range(self.mesh.n_tris):
|
|
896
|
-
itet = self.mesh.tri_to_tet[0,itri]
|
|
897
|
-
ertri[:,:,itri] = er[:,:,itet]
|
|
898
|
-
urtri[:,:,itri] = ur[:,:,itet]
|
|
899
|
-
condtri[itri] = cond[itet]
|
|
900
|
-
|
|
901
|
-
for freq, job in zip(self.frequencies, results):
|
|
902
903
|
|
|
904
|
+
for freq, job, mats in zip(self.frequencies, results, materials):
|
|
905
|
+
er, ur, cond = mats
|
|
906
|
+
ertri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
|
|
907
|
+
urtri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
|
|
908
|
+
condtri = np.zeros((self.mesh.n_tris,), dtype=np.complex128)
|
|
909
|
+
|
|
910
|
+
for itri in range(self.mesh.n_tris):
|
|
911
|
+
itet = self.mesh.tri_to_tet[0,itri]
|
|
912
|
+
ertri[:,:,itri] = er[:,:,itet]
|
|
913
|
+
urtri[:,:,itri] = ur[:,:,itet]
|
|
914
|
+
condtri[itri] = cond[0,0,itet]
|
|
915
|
+
|
|
903
916
|
k0 = 2*np.pi*freq/299792458
|
|
904
917
|
|
|
905
918
|
scalardata = self.data.scalar.new(freq=freq, **self._params)
|
|
@@ -73,7 +73,7 @@ class MWBoundaryConditionSet(BoundaryConditionSet):
|
|
|
73
73
|
"""
|
|
74
74
|
bcs = self.oftype(PEC)
|
|
75
75
|
for bc in self.oftype(SurfaceImpedance):
|
|
76
|
-
if bc.
|
|
76
|
+
if bc.sigma > 1e3:
|
|
77
77
|
bcs.append(bc)
|
|
78
78
|
|
|
79
79
|
return bcs
|
|
@@ -354,7 +354,7 @@ class FloquetPort(PortBC):
|
|
|
354
354
|
if cs is None:
|
|
355
355
|
cs = GCS
|
|
356
356
|
self.port_number: int= port_number
|
|
357
|
-
self.active: bool =
|
|
357
|
+
self.active: bool = False
|
|
358
358
|
self.power: float = power
|
|
359
359
|
self.type: str = 'TEM'
|
|
360
360
|
self.mode: tuple[int,int] = (1,0)
|
|
@@ -439,7 +439,6 @@ class ModalPort(PortBC):
|
|
|
439
439
|
def __init__(self,
|
|
440
440
|
face: FaceSelection | GeoSurface,
|
|
441
441
|
port_number: int,
|
|
442
|
-
active: bool = False,
|
|
443
442
|
cs: CoordinateSystem | None = None,
|
|
444
443
|
power: float = 1,
|
|
445
444
|
TEM: bool = False,
|
|
@@ -457,7 +456,6 @@ class ModalPort(PortBC):
|
|
|
457
456
|
Args:
|
|
458
457
|
face (FaceSelection, GeoSurface): The port mode face
|
|
459
458
|
port_number (int): The port number as an integer
|
|
460
|
-
active (bool, optional): Whether the port is set active. Defaults to False.
|
|
461
459
|
cs (CoordinateSystem, optional): The local coordinate system of the port face. Defaults to None.
|
|
462
460
|
power (float, optional): The radiated power. Defaults to 1.
|
|
463
461
|
TEM (bool, optional): Wether the mode should be considered as a TEM mode. Defaults to False
|
|
@@ -467,7 +465,7 @@ class ModalPort(PortBC):
|
|
|
467
465
|
super().__init__(face)
|
|
468
466
|
|
|
469
467
|
self.port_number: int= port_number
|
|
470
|
-
self.active: bool =
|
|
468
|
+
self.active: bool = False
|
|
471
469
|
self.power: float = power
|
|
472
470
|
self.alignment_vectors: list[Axis] = []
|
|
473
471
|
|
|
@@ -666,7 +664,7 @@ class RectangularWaveguide(PortBC):
|
|
|
666
664
|
def __init__(self,
|
|
667
665
|
face: FaceSelection | GeoSurface,
|
|
668
666
|
port_number: int,
|
|
669
|
-
|
|
667
|
+
mode: tuple[int, int] = (1,0),
|
|
670
668
|
cs: CoordinateSystem | None = None,
|
|
671
669
|
dims: tuple[float, float] | None = None,
|
|
672
670
|
power: float = 1):
|
|
@@ -681,7 +679,7 @@ class RectangularWaveguide(PortBC):
|
|
|
681
679
|
Args:
|
|
682
680
|
face (FaceSelection, GeoSurface): The port boundary face selection
|
|
683
681
|
port_number (int): The port number
|
|
684
|
-
|
|
682
|
+
mode: (tuple[int, int], optional): The TE mode number. Defaults to (1,0).
|
|
685
683
|
cs (CoordinateSystem, optional): The local coordinate system. Defaults to None.
|
|
686
684
|
dims (tuple[float, float], optional): The port face. Defaults to None.
|
|
687
685
|
power (float): The port power. Default to 1.
|
|
@@ -689,10 +687,10 @@ class RectangularWaveguide(PortBC):
|
|
|
689
687
|
super().__init__(face)
|
|
690
688
|
|
|
691
689
|
self.port_number: int= port_number
|
|
692
|
-
self.active: bool =
|
|
690
|
+
self.active: bool = False
|
|
693
691
|
self.power: float = power
|
|
694
692
|
self.type: str = 'TE'
|
|
695
|
-
self.mode: tuple[int,int] =
|
|
693
|
+
self.mode: tuple[int,int] = mode
|
|
696
694
|
|
|
697
695
|
if dims is None:
|
|
698
696
|
logger.info("Determining port face based on selection")
|
|
@@ -701,13 +699,12 @@ class RectangularWaveguide(PortBC):
|
|
|
701
699
|
self.dims = (width, height)
|
|
702
700
|
logger.debug(f'Port CS: {self.cs}')
|
|
703
701
|
logger.debug(f'Detected port {self.port_number} size = {width*1000:.1f} mm x {height*1000:.1f} mm')
|
|
704
|
-
|
|
702
|
+
else:
|
|
703
|
+
self.dims = dims
|
|
704
|
+
self.cs = cs
|
|
705
705
|
if self.cs is None:
|
|
706
706
|
logger.info('Constructing coordinate system from normal port')
|
|
707
707
|
self.cs = Axis(self.selection.normal).construct_cs()
|
|
708
|
-
else:
|
|
709
|
-
self.cs: CoordinateSystem = cs # type: ignore
|
|
710
|
-
|
|
711
708
|
def get_basis(self) -> np.ndarray:
|
|
712
709
|
return self.cs._basis
|
|
713
710
|
|
|
@@ -755,11 +752,12 @@ class RectangularWaveguide(PortBC):
|
|
|
755
752
|
|
|
756
753
|
width = self.dims[0]
|
|
757
754
|
height = self.dims[1]
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
755
|
+
m, n= self.mode
|
|
756
|
+
Ev = self.get_amplitude(k0)*np.cos(np.pi*m*(x_local)/width)*np.cos(np.pi*n*(y_local)/height)
|
|
757
|
+
Eh = self.get_amplitude(k0)*np.sin(np.pi*m*(x_local)/width)*np.sin(np.pi*n*(y_local)/height)
|
|
758
|
+
Ex = Eh
|
|
759
|
+
Ey = Ev
|
|
760
|
+
Ez = 0*Eh
|
|
763
761
|
Exyz = self._qmode(k0) * np.array([Ex, Ey, Ez])
|
|
764
762
|
return Exyz
|
|
765
763
|
|
|
@@ -787,7 +785,6 @@ class LumpedPort(PortBC):
|
|
|
787
785
|
width: float | None = None,
|
|
788
786
|
height: float | None = None,
|
|
789
787
|
direction: Axis | None = None,
|
|
790
|
-
active: bool = False,
|
|
791
788
|
power: float = 1,
|
|
792
789
|
Z0: float = 50):
|
|
793
790
|
"""Generates a lumped power boundary condition.
|
|
@@ -804,7 +801,6 @@ class LumpedPort(PortBC):
|
|
|
804
801
|
width (float): The port width (meters).
|
|
805
802
|
height (float): The port height (meters).
|
|
806
803
|
direction (Axis): The port direction as an Axis object (em.Axis(..) or em.ZAX)
|
|
807
|
-
active (bool, optional): Whether the port is active. Defaults to False.
|
|
808
804
|
power (float, optional): The port output power. Defaults to 1.
|
|
809
805
|
Z0 (float, optional): The port impedance. Defaults to 50.
|
|
810
806
|
"""
|
|
@@ -819,7 +815,7 @@ class LumpedPort(PortBC):
|
|
|
819
815
|
|
|
820
816
|
logger.debug(f'Lumped port: width={1000*width:.1f}mm, height={1000*height:.1f}mm, direction={direction}') # type: ignore
|
|
821
817
|
self.port_number: int= port_number
|
|
822
|
-
self.active: bool =
|
|
818
|
+
self.active: bool = False
|
|
823
819
|
|
|
824
820
|
self.power: float = power
|
|
825
821
|
self.Z0: float = Z0
|
|
@@ -1034,12 +1030,13 @@ class SurfaceImpedance(RobinBC):
|
|
|
1034
1030
|
self._material: Material | None = material
|
|
1035
1031
|
self._mur: float | complex = 1.0
|
|
1036
1032
|
self._epsr: float | complex = 1.0
|
|
1037
|
-
|
|
1038
1033
|
self.sigma: float = 0.0
|
|
1034
|
+
|
|
1039
1035
|
if material is not None:
|
|
1040
1036
|
self.sigma = material.cond
|
|
1041
1037
|
self._mur = material.ur
|
|
1042
1038
|
self._epsr = material.er
|
|
1039
|
+
|
|
1043
1040
|
if surface_conductance is not None:
|
|
1044
1041
|
self.sigma = surface_conductance
|
|
1045
1042
|
|
|
@@ -1066,10 +1063,15 @@ class SurfaceImpedance(RobinBC):
|
|
|
1066
1063
|
Returns:
|
|
1067
1064
|
complex: The γ-constant
|
|
1068
1065
|
"""
|
|
1066
|
+
|
|
1069
1067
|
w0 = k0*C0
|
|
1070
|
-
|
|
1068
|
+
f0 = w0/(2*np.pi)
|
|
1069
|
+
sigma = self.sigma.scalar(f0)
|
|
1070
|
+
mur = self._material.ur.scalar(f0)
|
|
1071
|
+
er = self._material.er.scalar(f0)
|
|
1072
|
+
|
|
1071
1073
|
rho = 1/sigma
|
|
1072
|
-
d_skin = (2*rho/(w0*MU0*
|
|
1074
|
+
d_skin = (2*rho/(w0*MU0*mur) * ((1+(w0*EPS0*er*rho)**2)**0.5 + rho*w0*EPS0*er))**0.5
|
|
1073
1075
|
R = rho/d_skin
|
|
1074
1076
|
if self._sr_model=='Hammerstad-Jensen' and self._sr > 0.0:
|
|
1075
1077
|
R = R * (1 + 2/np.pi * np.arctan(1.4*(self._sr/d_skin)**2))
|
|
@@ -20,7 +20,7 @@ from ...simulation_data import BaseDataset, DataContainer
|
|
|
20
20
|
from ...elements.femdata import FEMBasis
|
|
21
21
|
from dataclasses import dataclass
|
|
22
22
|
import numpy as np
|
|
23
|
-
from typing import Literal
|
|
23
|
+
from typing import Literal, Callable
|
|
24
24
|
from loguru import logger
|
|
25
25
|
from .adaptive_freq import SparamModel
|
|
26
26
|
from ...cs import Axis, _parse_axis
|
|
@@ -282,9 +282,9 @@ class FarFieldData:
|
|
|
282
282
|
|
|
283
283
|
@property
|
|
284
284
|
def Etheta(self) -> np.ndarray:
|
|
285
|
-
thx =
|
|
286
|
-
thy =
|
|
287
|
-
thz = np.sin(self.theta)
|
|
285
|
+
thx = np.cos(self.theta)*np.cos(self.phi)
|
|
286
|
+
thy = np.cos(self.theta)*np.sin(self.phi)
|
|
287
|
+
thz = -np.sin(self.theta)
|
|
288
288
|
return thx*self.E[0,:] + thy*self.E[1,:] + thz*self.E[2,:]
|
|
289
289
|
|
|
290
290
|
@property
|
|
@@ -296,11 +296,11 @@ class FarFieldData:
|
|
|
296
296
|
|
|
297
297
|
@property
|
|
298
298
|
def Erhcp(self) -> np.ndarray:
|
|
299
|
-
return (self.Etheta
|
|
299
|
+
return (self.Etheta + 1j*self.Ephi)/np.sqrt(2)
|
|
300
300
|
|
|
301
301
|
@property
|
|
302
302
|
def Elhcp(self) -> np.ndarray:
|
|
303
|
-
return (self.Etheta
|
|
303
|
+
return (self.Etheta - 1j*self.Ephi)/np.sqrt(2)
|
|
304
304
|
|
|
305
305
|
@property
|
|
306
306
|
def AR(self) -> np.ndarray:
|
|
@@ -702,6 +702,11 @@ class MWField:
|
|
|
702
702
|
|
|
703
703
|
def interpolate(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray) -> EHField:
|
|
704
704
|
''' Interpolate the dataset in the provided xs, ys, zs values'''
|
|
705
|
+
if isinstance(xs, (float, int, complex)):
|
|
706
|
+
xs = np.array([xs,])
|
|
707
|
+
ys = np.array([ys,])
|
|
708
|
+
zs = np.array([zs,])
|
|
709
|
+
|
|
705
710
|
shp = xs.shape
|
|
706
711
|
xf = xs.flatten()
|
|
707
712
|
yf = ys.flatten()
|
|
@@ -748,6 +753,7 @@ class MWField:
|
|
|
748
753
|
xs = np.linspace(xb[0], xb[1], int((xb[1]-xb[0])/ds))
|
|
749
754
|
ys = np.linspace(yb[0], yb[1], int((yb[1]-yb[0])/ds))
|
|
750
755
|
zs = np.linspace(zb[0], zb[1], int((zb[1]-zb[0])/ds))
|
|
756
|
+
|
|
751
757
|
if x is not None:
|
|
752
758
|
Y,Z = np.meshgrid(ys, zs)
|
|
753
759
|
X = x*np.ones_like(Y)
|
|
@@ -759,6 +765,56 @@ class MWField:
|
|
|
759
765
|
Z = z*np.ones_like(Y)
|
|
760
766
|
return self.interpolate(X,Y,Z)
|
|
761
767
|
|
|
768
|
+
def cutplane_normal(self,
|
|
769
|
+
point=(0,0,0),
|
|
770
|
+
normal=(0,0,1),
|
|
771
|
+
npoints: int = 300) -> EHField:
|
|
772
|
+
"""
|
|
773
|
+
Take a 2D slice of the field along an arbitrary plane.
|
|
774
|
+
Args:
|
|
775
|
+
point: (x0,y0,z0), a point on the plane
|
|
776
|
+
normal: (nx,ny,nz), plane normal vector
|
|
777
|
+
npoints: number of grid points per axis
|
|
778
|
+
"""
|
|
779
|
+
|
|
780
|
+
n = np.array(normal, dtype=float)
|
|
781
|
+
n /= np.linalg.norm(n)
|
|
782
|
+
point = np.array(point)
|
|
783
|
+
|
|
784
|
+
tmp = np.array([1,0,0]) if abs(n[0]) < 0.9 else np.array([0,1,0])
|
|
785
|
+
u = np.cross(n, tmp)
|
|
786
|
+
u /= np.linalg.norm(u)
|
|
787
|
+
v = np.cross(n, u)
|
|
788
|
+
|
|
789
|
+
xb, yb, zb = self.basis.bounds
|
|
790
|
+
nx, ny, nz = 5, 5, 5
|
|
791
|
+
Xg = np.linspace(xb[0], xb[1], nx)
|
|
792
|
+
Yg = np.linspace(yb[0], yb[1], ny)
|
|
793
|
+
Zg = np.linspace(zb[0], zb[1], nz)
|
|
794
|
+
Xg, Yg, Zg = np.meshgrid(Xg, Yg, Zg, indexing='ij')
|
|
795
|
+
geometry = np.vstack([Xg.ravel(), Yg.ravel(), Zg.ravel()]).T # Nx3
|
|
796
|
+
|
|
797
|
+
rel_pts = geometry - point
|
|
798
|
+
S = rel_pts @ u
|
|
799
|
+
T = rel_pts @ v
|
|
800
|
+
|
|
801
|
+
margin = 0.01
|
|
802
|
+
s_min, s_max = S.min(), S.max()
|
|
803
|
+
t_min, t_max = T.min(), T.max()
|
|
804
|
+
s_bounds = (s_min - margin*(s_max-s_min), s_max + margin*(s_max-s_min))
|
|
805
|
+
t_bounds = (t_min - margin*(t_max-t_min), t_max + margin*(t_max-t_min))
|
|
806
|
+
|
|
807
|
+
S_grid = np.linspace(s_bounds[0], s_bounds[1], npoints)
|
|
808
|
+
T_grid = np.linspace(t_bounds[0], t_bounds[1], npoints)
|
|
809
|
+
S_mesh, T_mesh = np.meshgrid(S_grid, T_grid)
|
|
810
|
+
|
|
811
|
+
X = point[0] + S_mesh*u[0] + T_mesh*v[0]
|
|
812
|
+
Y = point[1] + S_mesh*u[1] + T_mesh*v[1]
|
|
813
|
+
Z = point[2] + S_mesh*u[2] + T_mesh*v[2]
|
|
814
|
+
|
|
815
|
+
return self.interpolate(X, Y, Z)
|
|
816
|
+
|
|
817
|
+
|
|
762
818
|
def grid(self, ds: float) -> EHField:
|
|
763
819
|
"""Interpolate a uniform grid sampled at ds
|
|
764
820
|
|
|
@@ -962,7 +1018,8 @@ class MWField:
|
|
|
962
1018
|
k0 = self.k0
|
|
963
1019
|
return vertices, triangles, E, H, origin, k0
|
|
964
1020
|
|
|
965
|
-
def optycal_antenna(self,
|
|
1021
|
+
def optycal_antenna(self,
|
|
1022
|
+
faces: FaceSelection | GeoSurface | None = None,
|
|
966
1023
|
origin: tuple[float, float, float] | None = None,
|
|
967
1024
|
syms: list[Literal['Ex','Ey','Ez', 'Hx','Hy','Hz']] | None = None) -> dict:
|
|
968
1025
|
"""Export this models exterior to an Optical acceptable dataset
|
|
@@ -980,6 +1037,32 @@ class MWField:
|
|
|
980
1037
|
|
|
981
1038
|
return dict(freq=freq, ff_function=function)
|
|
982
1039
|
|
|
1040
|
+
# def surface_integral(self, faces: FaceSelection | GeoSurface, fieldfunction: Callable) -> float | complex:
|
|
1041
|
+
# """Computes a surface integral on the selected faces.
|
|
1042
|
+
|
|
1043
|
+
# The fieldfunction argument must be a callable of a single argument x, which will
|
|
1044
|
+
# be of type EHField which is restuned by the field.interpolate(x,y,z) function. It has
|
|
1045
|
+
# fields like Ez, Ey, Sx etc that can be called.
|
|
1046
|
+
|
|
1047
|
+
# Args:
|
|
1048
|
+
# faces (FaceSelection | GeoSurface): _description_
|
|
1049
|
+
# fieldfunction (Callable): _description_
|
|
1050
|
+
|
|
1051
|
+
# Returns:
|
|
1052
|
+
# float | complex: _description_
|
|
1053
|
+
# """
|
|
1054
|
+
# from ...mth.integrals import surface_integral
|
|
1055
|
+
|
|
1056
|
+
# def ff(x, y, z):
|
|
1057
|
+
# fieldobj = self.interpolate(x,y,z)
|
|
1058
|
+
# return fieldfunction(fieldobj)
|
|
1059
|
+
|
|
1060
|
+
# nodes = self.mesh.get_nodes(faces.tags)
|
|
1061
|
+
# triangles = self.mesh.get_triangles(faces.tags)
|
|
1062
|
+
|
|
1063
|
+
# return surface_integral(nodes, triangles, ff)
|
|
1064
|
+
|
|
1065
|
+
|
|
983
1066
|
class MWScalar:
|
|
984
1067
|
"""The MWDataSet class stores solution data of FEM Time Harmonic simulations.
|
|
985
1068
|
"""
|