emerge 0.6.6__py3-none-any.whl → 0.6.8__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/_emerge/mesh3d.py CHANGED
@@ -28,6 +28,7 @@ from loguru import logger
28
28
  from functools import cache
29
29
  from .bc import Periodic
30
30
  from .mth.pairing import pair_coordinates
31
+ from .material import Material
31
32
 
32
33
  @njit(f8(f8[:], f8[:], f8[:]), cache=True, nogil=True)
33
34
  def area(x1: np.ndarray, x2: np.ndarray, x3: np.ndarray):
@@ -538,10 +539,14 @@ class Mesh3D(Mesh):
538
539
  return conv_map, np.array(node_ids_2_unsorted), np.array(node_ids_2_sorted)
539
540
 
540
541
 
541
- def retreive(self, material_selector: Callable, volumes: list[GeoVolume]) -> np.ndarray:
542
+ def retreive(self, volumes: list[GeoVolume]) -> list[Material]:
542
543
  '''Retrieve the material properties of the geometry'''
543
- arry = np.zeros((3,3,self.n_tets,), dtype=np.complex128)
544
-
544
+ #arry = np.zeros((3,3,self.n_tets,), dtype=np.complex128)
545
+ for vol in volumes:
546
+ vol.material.reset()
547
+
548
+ materials = []
549
+
545
550
  xs = self.centers[0,:]
546
551
  ys = self.centers[1,:]
547
552
  zs = self.centers[2,:]
@@ -551,11 +556,12 @@ class Mesh3D(Mesh):
551
556
  for dimtag in volume.dimtags:
552
557
  etype, etag_list, ntags = gmsh.model.mesh.get_elements(*dimtag)
553
558
  for etags in etag_list:
554
- tet_ids = [self.tet_t2i[t] for t in etags]
555
-
556
- value = material_selector(volume.material, xs[tet_ids], ys[tet_ids], zs[tet_ids])
557
- arry[:,:,tet_ids] = value
558
- return arry
559
+ tet_ids = np.array([self.tet_t2i[t] for t in etags])
560
+ volume.material.initialize(xs[tet_ids], ys[tet_ids], zs[tet_ids], tet_ids)
561
+ if volume.material not in materials:
562
+ materials.append(volume.material)
563
+
564
+ return materials
559
565
 
560
566
  def plot_gmsh(self) -> None:
561
567
  gmsh.fltk.run()
@@ -21,7 +21,7 @@ from ....elements.nedelec2 import Nedelec2
21
21
  from ....elements.nedleg2 import NedelecLegrange2
22
22
  from ....mth.optimized import gaus_quad_tri
23
23
  from ....mth.pairing import pair_coordinates
24
-
24
+ from ....material import Material
25
25
  from scipy.sparse import csr_matrix
26
26
  from loguru import logger
27
27
  from ..simjob import SimJob
@@ -188,9 +188,7 @@ class Assembler:
188
188
  return E, B, np.array(solve_ids), nedlegfield
189
189
 
190
190
  def assemble_freq_matrix(self, field: Nedelec2,
191
- er: np.ndarray,
192
- ur: np.ndarray,
193
- sig: np.ndarray,
191
+ materials: list[Material],
194
192
  bcs: list[BoundaryCondition],
195
193
  frequency: float,
196
194
  cache_matrices: bool = False) -> SimJob:
@@ -215,11 +213,29 @@ class Assembler:
215
213
  # PREDEFINE CONSTANTS
216
214
  W0 = 2*np.pi*frequency
217
215
  K0 = W0/C0
218
-
216
+
217
+ is_frequency_dependent = False
219
218
  mesh = field.mesh
220
- er = er - 1j*sig/(W0*EPS0)*np.repeat(np.eye(3)[:, :, np.newaxis], er.shape[2], axis=2)
221
- is_frequency_dependent: bool = np.any((sig > 0) & (sig < self.conductivity_limit)) # type: ignore
219
+
220
+ for mat in materials:
221
+ if mat.frequency_dependent:
222
+ is_frequency_dependent = True
223
+ break
224
+
225
+ er = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
226
+ tand = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
227
+ cond = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
228
+ ur = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
229
+
230
+ for mat in materials:
231
+ er = mat.er(frequency, er)
232
+ ur = mat.ur(frequency, ur)
233
+ tand = mat.tand(frequency, tand)
234
+ cond = mat.cond(frequency, cond)
222
235
 
236
+ er = er*(1-1j*tand) - 1j*cond/(W0*EPS0)
237
+
238
+ is_frequency_dependent = is_frequency_dependent or np.any((cond > 0) & (cond < self.conductivity_limit)) # type: ignore
223
239
 
224
240
  if cache_matrices and not is_frequency_dependent and self.cached_matrices is not None:
225
241
  # IF CACHED AND AVAILABLE PULL E AND B FROM CACHE
@@ -253,10 +269,11 @@ class Assembler:
253
269
 
254
270
  logger.debug('Implementing PEC Boundary Conditions.')
255
271
  pec_ids: list[int] = []
272
+
256
273
  # Conductivity above al imit, consider it all PEC
257
274
  ipec = 0
258
275
  for itet in range(field.n_tets):
259
- if sig[itet] > self.conductivity_limit:
276
+ if cond[0,0,itet] > self.conductivity_limit:
260
277
  ipec+=1
261
278
  pec_ids.extend(field.tet_to_field[:,itet])
262
279
  if ipec>0:
@@ -388,12 +405,10 @@ class Assembler:
388
405
  simjob.Pd = Pmat.getH()
389
406
  simjob.has_periodic = has_periodic
390
407
 
391
- return simjob
408
+ return simjob, (er, ur, cond)
392
409
 
393
410
  def assemble_eig_matrix(self, field: Nedelec2,
394
- er: np.ndarray,
395
- ur: np.ndarray,
396
- sig: np.ndarray,
411
+ materials: list[Material],
397
412
  bcs: list[BoundaryCondition],
398
413
  frequency: float) -> SimJob:
399
414
  """Assembles the eigenmode analysis matrix
@@ -420,9 +435,21 @@ class Assembler:
420
435
  w0 = 2*np.pi*frequency
421
436
  k0 = w0/C0
422
437
 
423
- er = er - 1j*sig/(w0*EPS0)*np.repeat(np.eye(3)[:, :, np.newaxis], er.shape[2], axis=2)
438
+ er = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
439
+ tand = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
440
+ cond = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
441
+ ur = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
442
+
443
+ for mat in materials:
444
+ er = mat.er(frequency, er)
445
+ ur = mat.ur(frequency, ur)
446
+ tand = mat.tand(frequency, tand)
447
+ cond = mat.cond(frequency, cond)
448
+
449
+ er = er*(1-1j*tand) - 1j*cond/(w0*EPS0)
424
450
 
425
451
  logger.debug('Assembling matrices')
452
+
426
453
  E, B = tet_mass_stiffness_matrices(field, er, ur)
427
454
  self.cached_matrices = (E, B)
428
455
 
@@ -439,7 +466,7 @@ class Assembler:
439
466
 
440
467
  # Conductivity above a limit, consider it all PEC
441
468
  for itet in range(field.n_tets):
442
- if sig[itet] > self.conductivity_limit:
469
+ if cond[0,0,itet] > self.conductivity_limit:
443
470
  pec_ids.extend(field.tet_to_field[:,itet])
444
471
 
445
472
  # PEC Boundary conditions
@@ -461,11 +488,7 @@ class Assembler:
461
488
  # Robin BCs
462
489
  if len(robin_bcs) > 0:
463
490
  logger.debug('Implementing Robin Boundary Conditions.')
464
-
465
- if len(robin_bcs) > 0:
466
- logger.debug('Implementing Robin Boundary Conditions.')
467
-
468
- gauss_points = gaus_quad_tri(4)
491
+
469
492
  Bempty = field.empty_tri_matrix()
470
493
  for bc in robin_bcs:
471
494
 
@@ -550,4 +573,4 @@ class Assembler:
550
573
  simjob.Pd = Pmat.getH()
551
574
  simjob.has_periodic = has_periodic
552
575
 
553
- return simjob
576
+ return simjob, (er, ur, cond)
@@ -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
@@ -787,7 +787,6 @@ class LumpedPort(PortBC):
787
787
  width: float | None = None,
788
788
  height: float | None = None,
789
789
  direction: Axis | None = None,
790
- Idirection: Axis | None = None,
791
790
  active: bool = False,
792
791
  power: float = 1,
793
792
  Z0: float = 50):
@@ -814,7 +813,7 @@ class LumpedPort(PortBC):
814
813
  if width is None:
815
814
  if not isinstance(face, GeoObject):
816
815
  raise ValueError(f'The width, height and direction must be defined. Information cannot be extracted from {face}')
817
- width, height, direction, Idirection = face._data('width','height','vdir', 'idir')
816
+ width, height, direction = face._data('width','height','vdir')
818
817
  if width is None or height is None or direction is None:
819
818
  raise ValueError(f'The width, height and direction could not be extracted from {face}')
820
819
 
@@ -828,7 +827,6 @@ class LumpedPort(PortBC):
828
827
  self.width: float = width
829
828
  self.height: float = height # type: ignore
830
829
  self.Vdirection: Axis = direction # type: ignore
831
- self.Idirection: Axis = Idirection # type: ignore
832
830
  self.type = 'TEM'
833
831
 
834
832
  logger.info('Constructing coordinate system from normal port')
@@ -836,7 +834,6 @@ class LumpedPort(PortBC):
836
834
 
837
835
  self.vintline: Line | None = None
838
836
  self.v_integration = True
839
- self.iintline: Line | None = None
840
837
 
841
838
  @property
842
839
  def surfZ(self) -> float:
@@ -923,6 +920,7 @@ class LumpedPort(PortBC):
923
920
  Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
924
921
  return np.array([Exg, Eyg, Ezg])
925
922
 
923
+
926
924
  class LumpedElement(RobinBC):
927
925
 
928
926
  _include_stiff: bool = True
@@ -1036,12 +1034,13 @@ class SurfaceImpedance(RobinBC):
1036
1034
  self._material: Material | None = material
1037
1035
  self._mur: float | complex = 1.0
1038
1036
  self._epsr: float | complex = 1.0
1039
-
1040
1037
  self.sigma: float = 0.0
1038
+
1041
1039
  if material is not None:
1042
1040
  self.sigma = material.cond
1043
1041
  self._mur = material.ur
1044
1042
  self._epsr = material.er
1043
+
1045
1044
  if surface_conductance is not None:
1046
1045
  self.sigma = surface_conductance
1047
1046
 
@@ -1068,10 +1067,15 @@ class SurfaceImpedance(RobinBC):
1068
1067
  Returns:
1069
1068
  complex: The γ-constant
1070
1069
  """
1070
+
1071
1071
  w0 = k0*C0
1072
- sigma = self.sigma
1072
+ f0 = w0/(2*np.pi)
1073
+ sigma = self.sigma.scalar(f0)
1074
+ mur = self._material.ur.scalar(f0)
1075
+ er = self._material.er.scalar(f0)
1076
+
1073
1077
  rho = 1/sigma
1074
- d_skin = (2*rho/(w0*MU0*self._mur) * ((1+(w0*EPS0*self._epsr*rho)**2)**0.5 + rho*w0*EPS0*self._epsr))**0.5
1078
+ d_skin = (2*rho/(w0*MU0*mur) * ((1+(w0*EPS0*er*rho)**2)**0.5 + rho*w0*EPS0*er))**0.5
1075
1079
  R = rho/d_skin
1076
1080
  if self._sr_model=='Hammerstad-Jensen' and self._sr > 0.0:
1077
1081
  R = R * (1 + 2/np.pi * np.arctan(1.4*(self._sr/d_skin)**2))
@@ -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()
@@ -731,10 +736,24 @@ class MWField:
731
736
  x: float | None = None,
732
737
  y: float | None = None,
733
738
  z: float | None = None) -> EHField:
739
+ """Create a cartesian cut plane (XY, YZ or XZ) and compute the E and H-fields there
740
+
741
+ Only one coordiante and thus cutplane may be defined. If multiple are defined only the last (x->y->z) is used.
742
+
743
+ Args:
744
+ ds (float): The discretization step size
745
+ x (float | None, optional): The X-coordinate in case of a YZ-plane. Defaults to None.
746
+ y (float | None, optional): The Y-coordinate in case of an XZ-plane. Defaults to None.
747
+ z (float | None, optional): The Z-coordinate in case of an XY-plane. Defaults to None.
748
+
749
+ Returns:
750
+ EHField: The resultant EHField object
751
+ """
734
752
  xb, yb, zb = self.basis.bounds
735
753
  xs = np.linspace(xb[0], xb[1], int((xb[1]-xb[0])/ds))
736
754
  ys = np.linspace(yb[0], yb[1], int((yb[1]-yb[0])/ds))
737
755
  zs = np.linspace(zb[0], zb[1], int((zb[1]-zb[0])/ds))
756
+
738
757
  if x is not None:
739
758
  Y,Z = np.meshgrid(ys, zs)
740
759
  X = x*np.ones_like(Y)
@@ -746,6 +765,56 @@ class MWField:
746
765
  Z = z*np.ones_like(Y)
747
766
  return self.interpolate(X,Y,Z)
748
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
+
749
818
  def grid(self, ds: float) -> EHField:
750
819
  """Interpolate a uniform grid sampled at ds
751
820