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.

Files changed (33) hide show
  1. emerge/__init__.py +2 -2
  2. emerge/_emerge/_cache_check.py +1 -1
  3. emerge/_emerge/elements/femdata.py +3 -2
  4. emerge/_emerge/elements/index_interp.py +1 -2
  5. emerge/_emerge/elements/ned2_interp.py +16 -16
  6. emerge/_emerge/elements/nedelec2.py +17 -6
  7. emerge/_emerge/elements/nedleg2.py +21 -9
  8. emerge/_emerge/geo/__init__.py +1 -1
  9. emerge/_emerge/geo/horn.py +0 -1
  10. emerge/_emerge/geo/modeler.py +1 -1
  11. emerge/_emerge/geo/operations.py +13 -0
  12. emerge/_emerge/geo/pcb_tools/calculator.py +2 -3
  13. emerge/_emerge/geo/pmlbox.py +35 -11
  14. emerge/_emerge/geometry.py +13 -6
  15. emerge/_emerge/material.py +334 -82
  16. emerge/_emerge/mesh3d.py +14 -8
  17. emerge/_emerge/physics/microwave/assembly/assembler.py +43 -20
  18. emerge/_emerge/physics/microwave/microwave_3d.py +57 -44
  19. emerge/_emerge/physics/microwave/microwave_bc.py +26 -24
  20. emerge/_emerge/physics/microwave/microwave_data.py +90 -7
  21. emerge/_emerge/plot/pyvista/display.py +53 -15
  22. emerge/_emerge/plot/pyvista/display_settings.py +4 -1
  23. emerge/_emerge/plot/simple_plots.py +42 -26
  24. emerge/_emerge/projects/_load_base.txt +1 -2
  25. emerge/_emerge/selection.py +4 -0
  26. emerge/_emerge/simmodel.py +21 -9
  27. emerge/_emerge/solver.py +45 -18
  28. emerge/lib.py +256 -250
  29. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/METADATA +2 -1
  30. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/RECORD +33 -33
  31. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/licenses/LICENSE +2 -2
  32. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/WHEEL +0 -0
  33. {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
- ertet = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
471
- urtet = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
472
- condtet = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
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
- # Exy = np.max(np.max(Emode))
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-3 and not TEM:
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
- er = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
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, er, ur, cond,
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, er, ur, cond,
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, er, ur, cond,
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, er, ur, cond)
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
- er = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
821
- ur = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
822
- cond = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
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, er, ur, cond,
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
- Emode = np.zeros((self.basis.n_field,), dtype=np.complex128)
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(freq=eig_freq, **self._params)
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(freq=eig_freq, **self._params)
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], er: np.ndarray, ur: np.ndarray, cond: np.ndarray):
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.material.cond > 1e3:
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 = True
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 = active
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
- active: bool = False,
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
- active (bool, optional): Ther the port is active. Defaults to False.
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 = active
690
+ self.active: bool = False
693
691
  self.power: float = power
694
692
  self.type: str = 'TE'
695
- self.mode: tuple[int,int] = (1,0)
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
- E = self.get_amplitude(k0)*np.cos(np.pi*self.mode[0]*(x_local)/width)*np.cos(np.pi*self.mode[1]*(y_local)/height)
760
- Ex = 0*E
761
- Ey = E
762
- Ez = 0*E
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 = active
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
- sigma = self.sigma
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*self._mur) * ((1+(w0*EPS0*self._epsr*rho)**2)**0.5 + rho*w0*EPS0*self._epsr))**0.5
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 = -np.cos(self.theta)*np.cos(self.phi)
286
- thy = -np.cos(self.theta)*np.sin(self.phi)
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 - 1j*self.Ephi)/np.sqrt(2)
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 + 1j*self.Ephi)/np.sqrt(2)
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, faces: FaceSelection | GeoSurface | None = None,
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
  """