emerge 0.5.5__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of emerge might be problematic. Click here for more details.

Files changed (39) hide show
  1. emerge/__init__.py +4 -1
  2. emerge/_emerge/cs.py +2 -2
  3. emerge/_emerge/elements/ned2_interp.py +21 -26
  4. emerge/_emerge/elements/nedleg2.py +27 -45
  5. emerge/_emerge/geo/__init__.py +1 -1
  6. emerge/_emerge/geo/modeler.py +2 -2
  7. emerge/_emerge/geo/pcb.py +4 -4
  8. emerge/_emerge/geo/shapes.py +37 -14
  9. emerge/_emerge/geometry.py +27 -1
  10. emerge/_emerge/howto.py +9 -9
  11. emerge/_emerge/material.py +1 -0
  12. emerge/_emerge/mesh3d.py +63 -14
  13. emerge/_emerge/mesher.py +7 -4
  14. emerge/_emerge/mth/optimized.py +30 -0
  15. emerge/_emerge/periodic.py +46 -16
  16. emerge/_emerge/physics/microwave/assembly/assembler.py +4 -21
  17. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +23 -19
  18. emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +465 -0
  19. emerge/_emerge/physics/microwave/assembly/robinbc.py +59 -18
  20. emerge/_emerge/physics/microwave/microwave_3d.py +38 -186
  21. emerge/_emerge/physics/microwave/microwave_bc.py +101 -35
  22. emerge/_emerge/physics/microwave/microwave_data.py +1 -1
  23. emerge/_emerge/plot/pyvista/display.py +40 -7
  24. emerge/_emerge/plot/pyvista/display_settings.py +1 -0
  25. emerge/_emerge/plot/simple_plots.py +159 -27
  26. emerge/_emerge/projects/_gen_base.txt +2 -2
  27. emerge/_emerge/projects/_load_base.txt +1 -1
  28. emerge/_emerge/simmodel.py +22 -7
  29. emerge/_emerge/solve_interfaces/cudss_interface.py +44 -2
  30. emerge/_emerge/solve_interfaces/pardiso_interface.py +1 -0
  31. emerge/_emerge/solver.py +26 -19
  32. emerge/ext.py +4 -0
  33. emerge/lib.py +1 -1
  34. {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/METADATA +6 -4
  35. {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/RECORD +38 -37
  36. emerge/_emerge/elements/legrange2.py +0 -172
  37. {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/WHEEL +0 -0
  38. {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/entry_points.txt +0 -0
  39. {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -24,6 +24,7 @@ from ...elements.nedelec2 import Nedelec2
24
24
  from ...solver import DEFAULT_ROUTINE, SolveRoutine
25
25
  from ...system import called_from_main_function
26
26
  from ...selection import FaceSelection
27
+ from ...mth.optimized import compute_distances
27
28
  from .microwave_bc import MWBoundaryConditionSet, PEC, ModalPort, LumpedPort, PortBC
28
29
  from .microwave_data import MWData
29
30
  from .assembly.assembler import Assembler
@@ -60,7 +61,7 @@ def run_job_multi(job: SimJob) -> SimJob:
60
61
  return job
61
62
 
62
63
 
63
- def _dimstring(data: list[float]) -> str:
64
+ def _dimstring(data: list[float] | np.ndarray) -> str:
64
65
  """A String formatter for dimensions in millimeters
65
66
 
66
67
  Args:
@@ -99,6 +100,21 @@ def shortest_path(xyz1: np.ndarray, xyz2: np.ndarray, Npts: int) -> np.ndarray:
99
100
 
100
101
  return path
101
102
 
103
+ def _pick_central(vertices: np.ndarray) -> np.ndarray:
104
+ """Computes the coordinate in the vertex set that has the shortest square distance to all other points.
105
+
106
+
107
+ Args:
108
+ vertices (np.ndarray): The set of coordinates [3,:]
109
+
110
+ Returns:
111
+ np.ndarray: The most central point
112
+ """
113
+ Ds = compute_distances(vertices[0,:], vertices[1,:], vertices[2,:])
114
+ sumDs = np.sum(Ds**2, axis=1)
115
+ id_central = np.argwhere(sumDs==np.min(sumDs)).flatten()[0]
116
+ return vertices[:, id_central].squeeze()
117
+
102
118
  class Microwave3D:
103
119
  """The Electrodynamics time harmonic physics class.
104
120
 
@@ -302,8 +318,8 @@ class Microwave3D:
302
318
  dotprod = xs*field_axis[0] + ys*field_axis[1] + zs*field_axis[2]
303
319
 
304
320
  start_id = points[np.argwhere(dotprod == np.min(dotprod))]
305
-
306
- start = np.squeeze(np.mean(self.mesh.nodes[:,start_id],axis=1))
321
+
322
+ start = _pick_central(self.mesh.nodes[:,start_id.flatten()])
307
323
  logger.info(f'Starting node = {_dimstring(start)}')
308
324
  end = start + port.Vdirection.np*port.height
309
325
 
@@ -493,6 +509,7 @@ class Microwave3D:
493
509
  if TEM:
494
510
  target_kz = ermean*urmean*1.1*k0
495
511
  else:
512
+
496
513
  target_kz = ermean*urmean*0.7*k0
497
514
 
498
515
 
@@ -514,7 +531,8 @@ class Microwave3D:
514
531
  Emode[solve_ids] = np.squeeze(eigenmode)
515
532
  Emode = Emode * np.exp(-1j*np.angle(np.max(Emode)))
516
533
 
517
- beta = min(k0*np.sqrt(ermax*urmax), np.emath.sqrt(-eigen_values[i]))
534
+ beta_base = np.emath.sqrt(-eigen_values[i])
535
+ beta = min(k0*np.sqrt(ermax*urmax), beta_base)
518
536
 
519
537
  residuals = -1
520
538
 
@@ -559,14 +577,14 @@ class Microwave3D:
559
577
  logger.info(f'Elapsed time = {(T2-T0):.2f} seconds.')
560
578
  return None
561
579
 
562
- def frequency_domain(self,
563
- parallel: bool = False,
564
- njobs: int = 2,
565
- harddisc_threshold: int | None = None,
566
- harddisc_path: str = 'EMergeSparse',
567
- frequency_groups: int = -1,
568
- multi_processing: bool = False,
569
- automatic_modal_analysis: bool = True) -> MWData:
580
+ def run_sweep(self,
581
+ parallel: bool = False,
582
+ njobs: int = 2,
583
+ harddisc_threshold: int | None = None,
584
+ harddisc_path: str = 'EMergeSparse',
585
+ frequency_groups: int = -1,
586
+ multi_processing: bool = False,
587
+ automatic_modal_analysis: bool = True) -> MWData:
570
588
  """Executes a frequency domain study
571
589
 
572
590
  The study is distributed over "njobs" workers.
@@ -999,179 +1017,13 @@ class Microwave3D:
999
1017
  mode_p = sparam_mode_power(self.mesh.nodes, tri_vertices, bc, k0, const, 5)
1000
1018
  return field_p, mode_p
1001
1019
 
1002
- # def frequency_domain_single(self, automatic_modal_analysis: bool = False) -> MWData:
1003
- # """Execute a frequency domain study without distributed frequency sweep.
1004
-
1005
- # Args:
1006
- # automatic_modal_analysis (bool, optional): Automatically compute port modes. Defaults to False.
1007
-
1008
- # Raises:
1009
- # SimulationError: _description_
1010
-
1011
- # Returns:
1012
- # MWSimData: The Simulation data.
1013
- # """
1014
- # T0 = time.time()
1015
- # mesh = self.mesh
1016
- # if self.bc._initialized is False:
1017
- # raise SimulationError('Cannot run a modal analysis because no boundary conditions have been assigned.')
1018
-
1019
- # self._initialize_field()
1020
- # self._initialize_bc_data()
1021
-
1022
- # er = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
1023
- # ur = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
1024
- # cond = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
1025
-
1026
- # ertri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
1027
- # urtri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
1028
- # condtri = np.zeros((self.mesh.n_tris,), dtype=np.complex128)
1029
-
1030
- # for itri in range(self.mesh.n_tris):
1031
- # itet = self.mesh.tri_to_tet[0,itri]
1032
- # ertri[:,:,itri] = er[:,:,itet]
1033
- # urtri[:,:,itri] = ur[:,:,itet]
1034
- # condtri[itri] = cond[itet]
1035
-
1036
- # #### Port settings
1037
-
1038
- # all_ports = self.bc.oftype(PortBC)
1039
- # port_numbers = [port.port_number for port in all_ports]
1040
-
1041
- # ##### FOR PORT SWEEP SET ALL ACTIVE TO FALSE. THIS SHOULD BE FIXED LATER
1042
- # ### COMPUTE WHICH TETS ARE CONNECTED TO PORT INDICES
1043
-
1044
- # all_port_tets = []
1045
- # for port in all_ports:
1046
- # port.active=False
1047
-
1048
- # all_port_tets = mesh.get_face_tets(*[port.tags for port in all_ports])
1049
-
1050
-
1051
- # logger.debug(f'Starting the simulation of {len(self.frequencies)} frequency points.')
1052
-
1053
- # # ITERATE OVER FREQUENCIES
1054
- # for freq in self.frequencies:
1055
- # logger.info(f'Simulation frequency = {freq/1e9:.3f} GHz')
1056
-
1057
- # # Assembling matrix problem
1058
- # if automatic_modal_analysis:
1059
- # self._compute_modes(freq)
1060
-
1061
- # job = self.assembler.assemble_freq_matrix(self.basis, er, ur, cond, self.bc.boundary_conditions, freq, cache_matrices=self.cache_matrices)
1062
-
1063
- # logger.debug(f'Routine: {self.solveroutine}')
1064
1020
 
1065
- # for A, b, ids, reuse in job.iter_Ab():
1066
- # solution, report = self.solveroutine.solve(A, b, ids, reuse)
1067
- # job.submit_solution(solution, report)
1021
+ ############################################################
1022
+ # DEPRICATED FUNCTIONS #
1023
+ ############################################################
1068
1024
 
1069
- # self.data.setreport(job.reports, freq=freq, **self._params)
1070
-
1071
- # k0 = 2*np.pi*freq/299792458
1072
-
1073
- # scalardata = self.data.scalar.new(freq=freq, **self._params)
1074
- # scalardata.init_sp(port_numbers)
1075
- # scalardata.freq = freq
1076
- # scalardata.k0 = k0
1077
-
1078
- # fielddata = self.data.field.new(freq=freq, **self._params)
1079
- # fielddata.freq = freq
1080
- # fielddata.er = np.squeeze(er[0,0,:])
1081
- # fielddata.ur = np.squeeze(ur[0,0,:])
1082
-
1083
- # # Recording port information
1084
- # for i, port in enumerate(all_ports):
1085
- # fielddata.add_port_properties(port.port_number,
1086
- # mode_number=port.mode_number,
1087
- # k0 = k0,
1088
- # beta = port.get_beta(k0),
1089
- # Z0 = port.portZ0(k0),
1090
- # Pout= port.power)
1091
-
1092
- # for active_port in all_ports:
1093
-
1094
- # active_port.active = True
1095
- # solution = job._fields[active_port.port_number]
1096
-
1097
- # fielddata._fields[active_port.port_number] = solution # TODO: THIS IS VERY FRAIL
1098
- # fielddata.basis = self.basis
1099
-
1100
- # # Compute the S-parameters
1101
- # # Define the field interpolation function
1102
- # fieldf = self.basis.interpolate_Ef(solution, tetids=all_port_tets)
1103
-
1104
- # # Active port power
1105
- # logger.debug('Active ports:')
1106
- # tris = mesh.get_triangles(active_port.tags)
1107
- # tri_vertices = mesh.tris[:,tris]
1108
- # pfield, pmode = self._compute_s_data(active_port, fieldf, tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
1109
- # logger.debug(f'Field Amplitude = {np.abs(pfield):.3f}, Excitation = {np.abs(pmode):.2f}')
1110
- # Pout = pmode
1111
-
1112
- # #Passive ports
1113
- # logger.debug('Passive ports:')
1114
- # for bc in all_ports:
1115
- # tris = mesh.get_triangles(bc.tags)
1116
- # tri_vertices = mesh.tris[:,tris]
1117
- # pfield, pmode = self._compute_s_data(bc, fieldf, tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
1118
- # logger.debug(f'Field amplitude = {np.abs(pfield):.3f}, Excitation= {np.abs(pmode):.2f}')
1119
- # scalardata.write_S(bc.port_number, active_port.port_number, pfield/Pout)
1120
-
1121
- # active_port.active=False
1122
-
1123
- # fielddata.set_field_vector()
1124
-
1125
- # logger.info('Simulation Complete!')
1126
- # T2 = time.time()
1127
- # logger.info(f'Elapsed time = {(T2-T0):.2f} seconds.')
1128
- # return self.data
1129
- ## DEPRICATED
1130
-
1131
-
1132
-
1133
- #
1134
- # def eigenmode(self, mesh: Mesh3D, solver = None, num_sols: int = 6):
1135
- # if solver is None:
1136
- # logger.info('Defaulting to BiCGStab.')
1137
- # solver = sparse.linalg.eigs
1138
-
1139
- # if self.order == 1:
1140
- # logger.info('Detected 1st order elements.')
1141
- # from ...elements.nedelec1.assembly import assemble_eig_matrix
1142
- # ft = FieldType.VEC_LIN
1143
-
1144
- # elif self.order == 2:
1145
- # logger.info('Detected 2nd order elements.')
1146
- # from ...elements.nedelec2.assembly import assemble_eig_matrix_E
1147
- # ft = FieldType.VEC_QUAD
1148
-
1149
- # er = self.mesh.retreive(mesh.centers, lambda mat,x,y,z: mat.fer3d(x,y,z))
1150
- # ur = self.mesh.retreive(mesh.centers, lambda mat,x,y,z: mat.fur3d(x,y,z))
1151
-
1152
- # dataset = Dataset3D(mesh, self.frequencies, 0, ft)
1153
- # dataset.er = er
1154
- # dataset.ur = ur
1155
- # logger.info('Solving eigenmodes.')
1156
-
1157
- # f_target = self.frequencies[0]
1158
- # sigma = (2 * np.pi * f_target / 299792458)**2
1159
-
1160
- # A, B, solvenodes = assemble_eig_matrix(mesh, er, ur, self.boundary_conditions)
1161
-
1162
- # A = A[np.ix_(solvenodes, solvenodes)]
1163
- # B = B[np.ix_(solvenodes, solvenodes)]
1164
- # #A = sparse.csc_matrix(A)
1165
- # #B = sparse.csc_matrix(B)
1166
-
1167
- # w, v = sparse.linalg.eigs(A, k=num_sols, M=B, sigma=sigma, which='LM')
1168
-
1169
- # logger.info(f'Eigenvalues: {np.sqrt(w)*299792458/(2*np.pi) * 1e-9} GHz')
1170
-
1171
- # Esol = np.zeros((num_sols, mesh.nfield), dtype=np.complex128)
1172
-
1173
- # Esol[:, solvenodes] = v.T
1174
-
1175
- # dataset.set_efield(Esol)
1176
-
1177
- # self.basis = dataset
1025
+ def frequency_domain(self, *args, **kwargs):
1026
+ """DEPRICATED VERSION: Use run_sweep() instead.
1027
+ """
1028
+ logger.warning('This function is depricated. Please use run_sweep() instead')
1029
+ self.run_sweep(*args, **kwargs)
@@ -20,7 +20,7 @@ import numpy as np
20
20
  from loguru import logger
21
21
  from typing import Callable, Literal
22
22
  from ...selection import Selection, FaceSelection
23
- from ...cs import CoordinateSystem, Axis, GCS
23
+ from ...cs import CoordinateSystem, Axis, GCS, _parse_axis
24
24
  from ...coord import Line
25
25
  from ...geometry import GeoSurface, GeoObject
26
26
  from dataclasses import dataclass
@@ -28,7 +28,23 @@ from collections import defaultdict
28
28
  from ...bc import BoundaryCondition, BoundaryConditionSet, Periodic
29
29
  from ...periodic import PeriodicCell, HexCell, RectCell
30
30
  from ...material import Material
31
- from ...const import Z0, C0, PI, EPS0, MU0
31
+ from ...const import Z0, C0, EPS0, MU0
32
+
33
+
34
+
35
+ ############################################################
36
+ # UTILITY FUNCTIONS #
37
+ ############################################################
38
+
39
+ def _inner_product(function: Callable, x: np.ndarray, y: np.ndarray, z: np.ndarray, ax: Axis) -> float:
40
+ Exyz = function(x,y,z)
41
+ return np.sum(Exyz[0,:]*ax.x + Exyz[1,:]*ax.y + Exyz[2,:]*ax.z)
42
+
43
+
44
+
45
+ ############################################################
46
+ # MAIN BC MANAGER CLASS #
47
+ ############################################################
32
48
 
33
49
  class MWBoundaryConditionSet(BoundaryConditionSet):
34
50
 
@@ -83,6 +99,10 @@ class MWBoundaryConditionSet(BoundaryConditionSet):
83
99
  return port
84
100
 
85
101
 
102
+ ############################################################
103
+ # BOUNDARY CONDITIONS #
104
+ ############################################################
105
+
86
106
 
87
107
  class PEC(BoundaryCondition):
88
108
 
@@ -313,7 +333,7 @@ class PortMode:
313
333
  self.energy = np.mean(np.abs(self.modefield)**2)
314
334
 
315
335
  def __str__(self):
316
- return f'PortMode(k0={self.k0}, beta={self.beta}, neff={self.neff}, energy={self.energy})'
336
+ return f'PortMode(k0={self.k0}, beta={self.beta}({self.neff:.3f}))'
317
337
 
318
338
  def set_power(self, power: complex) -> None:
319
339
  self.norm_factor = np.sqrt(1/np.abs(power))
@@ -371,8 +391,8 @@ class FloquetPort(PortBC):
371
391
  """
372
392
  return 1j*self.get_beta(k0)
373
393
 
374
- def get_Uinc(self, x_local: np.ndarray, y_local: np.ndarray, k0: float) -> np.ndarray:
375
- return -2*1j*self.get_beta(k0)*self.port_mode_3d(x_local, y_local, k0)
394
+ def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0: float) -> np.ndarray:
395
+ return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
376
396
 
377
397
  def port_mode_3d(self,
378
398
  x_local: np.ndarray,
@@ -449,7 +469,7 @@ class ModalPort(PortBC):
449
469
  self.port_number: int= port_number
450
470
  self.active: bool = active
451
471
  self.power: float = power
452
-
472
+ self.alignment_vectors: list[Axis] = []
453
473
 
454
474
  self.selected_mode: int = 0
455
475
  self.modes: dict[float, list[PortMode]] = defaultdict(list)
@@ -459,6 +479,7 @@ class ModalPort(PortBC):
459
479
  self.initialized: bool = False
460
480
  self._first_k0: float | None = None
461
481
  self._last_k0: float | None = None
482
+
462
483
 
463
484
  if cs is None:
464
485
  logger.info('Constructing coordinate system from normal port')
@@ -474,6 +495,17 @@ class ModalPort(PortBC):
474
495
  def modetype(self, k0: float) -> Literal['TEM','TE','TM']:
475
496
  return self.get_mode(k0).modetype
476
497
 
498
+ def align_modes(self, *axes: tuple | np.ndarray | Axis) -> None:
499
+ """Set a reriees of Axis objects that define a sequence of mode field
500
+ alignments.
501
+
502
+ The modes will be sorted to maximize the inner product: |∬ E(x,y) · ax dS|
503
+
504
+ Args:
505
+ *axes (tuple, np.ndarray, Axis): The alignment vectors.
506
+ """
507
+ self.alignment_vectors = [_parse_axis(ax) for ax in axes]
508
+
477
509
  @property
478
510
  def nmodes(self) -> int:
479
511
  if self._last_k0 is None:
@@ -488,6 +520,26 @@ class ModalPort(PortBC):
488
520
  def sort_modes(self) -> None:
489
521
  """Sorts the port modes based on total energy
490
522
  """
523
+
524
+ if len(self.alignment_vectors) > 0:
525
+ logger.trace(f'Sorting modes based on alignment vectors: {self.alignment_vectors}')
526
+ X, Y, Z = self.selection.sample(5)
527
+ X = X.flatten()
528
+ Y = Y.flatten()
529
+ Z = Z.flatten()
530
+ for k0, modes in self.modes.items():
531
+ logger.trace(f'Aligning modes for k0={k0:.3f} rad/m')
532
+ new_modes = []
533
+ for ax in self.alignment_vectors:
534
+ logger.trace(f'.mode vector {ax}')
535
+ integrals = [_inner_product(m.E_function, X, Y, Z, ax) for m in modes]
536
+ integral, opt_mode = sorted([pair for pair in zip(integrals, modes)], key=lambda x: abs(x[0]), reverse=True)[0]
537
+ opt_mode.polarity = np.sign(integral.real)
538
+ logger.trace(f'Optimal mode = {opt_mode} ({integral}), polarization alignment = {opt_mode.polarity}')
539
+ new_modes.append(opt_mode)
540
+
541
+ self.modes[k0] = new_modes
542
+ return
491
543
  for k0, modes in self.modes.items():
492
544
  self.modes[k0] = sorted(modes, key=lambda m: m.energy, reverse=True)
493
545
 
@@ -578,8 +630,8 @@ class ModalPort(PortBC):
578
630
  def get_gamma(self, k0: float) -> complex:
579
631
  return 1j*self.get_beta(k0)
580
632
 
581
- def get_Uinc(self, x_local, y_local, k0) -> np.ndarray:
582
- return -2*1j*self.get_beta(k0)*self.port_mode_3d(x_local, y_local, k0)
633
+ def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0) -> np.ndarray:
634
+ return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
583
635
 
584
636
  def port_mode_3d(self,
585
637
  x_local: np.ndarray,
@@ -691,8 +743,8 @@ class RectangularWaveguide(PortBC):
691
743
  """
692
744
  return 1j*self.get_beta(k0)
693
745
 
694
- def get_Uinc(self, x_local: np.ndarray, y_local: np.ndarray, k0: float) -> np.ndarray:
695
- return -2*1j*self.get_beta(k0)*self.port_mode_3d(x_local, y_local, k0)
746
+ def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0: float) -> np.ndarray:
747
+ return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
696
748
 
697
749
  def port_mode_3d(self,
698
750
  x_local: np.ndarray,
@@ -826,9 +878,9 @@ class LumpedPort(PortBC):
826
878
  """
827
879
  return 1j*k0*Z0/self.surfZ
828
880
 
829
- def get_Uinc(self, x_local, y_local, k0) -> np.ndarray:
881
+ def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0) -> np.ndarray:
830
882
  Emag = -1j*2*k0 * self.voltage/self.height * (Z0/self.surfZ)
831
- return Emag*self.port_mode_3d(x_local, y_local, k0)
883
+ return Emag*self.port_mode_3d_global(x_global, y_global, z_global, k0)
832
884
 
833
885
  def port_mode_3d(self,
834
886
  x_local: np.ndarray,
@@ -871,7 +923,6 @@ class LumpedPort(PortBC):
871
923
  Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
872
924
  return np.array([Exg, Eyg, Ezg])
873
925
 
874
-
875
926
  class LumpedElement(RobinBC):
876
927
 
877
928
  _include_stiff: bool = True
@@ -948,8 +999,6 @@ class LumpedElement(RobinBC):
948
999
  """
949
1000
  return 1j*k0*Z0/self.surfZ(k0)
950
1001
 
951
-
952
-
953
1002
  class SurfaceImpedance(RobinBC):
954
1003
 
955
1004
  _include_stiff: bool = True
@@ -959,29 +1008,45 @@ class SurfaceImpedance(RobinBC):
959
1008
  def __init__(self,
960
1009
  face: FaceSelection | GeoSurface,
961
1010
  material: Material | None = None,
1011
+ surface_conductance: float | None = None,
1012
+ surface_roughness: float = 0,
1013
+ sr_model: Literal['Hammerstad-Jensen'] = 'Hammerstad-Jensen',
962
1014
  ):
963
- """Generates a lumped power boundary condition.
964
-
965
- The lumped port boundary condition assumes a uniform E-field along the "direction" axis.
966
- The port with and height must be provided manually in meters. The height is the size
967
- in the "direction" axis along which the potential is imposed. The width dimension
968
- is orthogonal to that. For a rectangular face its the width and for a cyllindrical face
969
- its the circumpherance.
1015
+ """Generates a SurfaceImpedance bounary condition.
1016
+
1017
+ The surface impedance model treats a 2D surface selection as a finite conductor. It is not
1018
+ intended to be used for dielectric materials.
1019
+
1020
+ The surface resistivity is computed based on the material properties: σ, ε and μ.
1021
+
1022
+ The user may also supply the surface condutivity directly.
1023
+
1024
+ Optionally, a surface roughness in meters RMS may be supplied. In the current implementation
1025
+ The Hammersstad-Jensen model is used increasing the resistivity by a factor (1 + 2/π tan⁻¹(1.4(Δ/δ)²).
970
1026
 
971
1027
  Args:
972
- face (FaceSelection, GeoSurface): The port surface
973
- port_number (int): The port number
974
- width (float): The port width (meters).
975
- height (float): The port height (meters).
976
- direction (Axis): The port direction as an Axis object (em.Axis(..) or em.ZAX)
977
- active (bool, optional): Whether the port is active. Defaults to False.
978
- power (float, optional): The port output power. Defaults to 1.
979
- Z0 (float, optional): The port impedance. Defaults to 50.
1028
+ face (FaceSelection | GeoSurface): The face to apply this condition to.
1029
+ material (Material | None, optional): The matrial to assign. Defaults to None.
1030
+ surface_conductance (float | None, optional): The specific bulk conductivity to use. Defaults to None.
1031
+ surface_roughness (float, optional): The surface roughness. Defaults to 0.
1032
+ sr_model (Literal['Hammerstad, optional): The surface roughness model. Defaults to 'Hammerstad-Jensen'.
980
1033
  """
981
1034
  super().__init__(face)
982
1035
 
983
- self.material: Material = material
984
-
1036
+ self._material: Material | None = material
1037
+ self._mur: float | complex = 1.0
1038
+ self._epsr: float | complex = 1.0
1039
+
1040
+ self.sigma: float = 0.0
1041
+ if material is not None:
1042
+ self.sigma = material.cond
1043
+ self._mur = material.ur
1044
+ self._epsr = material.er
1045
+ if surface_conductance is not None:
1046
+ self.sigma = surface_conductance
1047
+
1048
+ self._sr: float = surface_roughness
1049
+ self._sr_model: str = sr_model
985
1050
 
986
1051
  def get_basis(self) -> np.ndarray | None:
987
1052
  return None
@@ -1004,9 +1069,10 @@ class SurfaceImpedance(RobinBC):
1004
1069
  complex: The γ-constant
1005
1070
  """
1006
1071
  w0 = k0*C0
1007
- sigma = self.material.cond
1072
+ sigma = self.sigma
1008
1073
  rho = 1/sigma
1009
- d_skin = (2*rho/(w0*MU0) * ((1+(w0*EPS0*rho)**2)**0.5 + rho*w0*EPS0))**0.5
1010
- d_skin = (2*rho/(w0*MU0))**0.5
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
1011
1075
  R = rho/d_skin
1076
+ if self._sr_model=='Hammerstad-Jensen' and self._sr > 0.0:
1077
+ R = R * (1 + 2/np.pi * np.arctan(1.4*(self._sr/d_skin)**2))
1012
1078
  return 1j*k0*Z0/R
@@ -777,7 +777,7 @@ class MWField:
777
777
  syms (list[Literal['Ex','Ey','Ez','Hx','Hy','Hz']], optional): E and H-plane symmetry planes where Ex is E-symmetry in x=0. Defaults to []
778
778
 
779
779
  Returns:
780
- tuple[np.ndarray, np.ndarray, np.ndarray]: _description_
780
+ tuple[np.ndarray, np.ndarray, np.ndarray]: Angles (N,), E(3,N), H(3,N)
781
781
  """
782
782
  refdir = _parse_axis(ref_direction).np
783
783
  plane_normal_parsed = _parse_axis(plane_normal).np
@@ -213,8 +213,18 @@ class PVDisplay(BaseDisplay):
213
213
  self._plot.add_key_event("m", self.activate_ruler) # type: ignore
214
214
  self._plot.add_key_event("f", self.activate_object) # type: ignore
215
215
 
216
- self._ctr: int = 0
217
-
216
+ self._ctr: int = 0
217
+
218
+ self.camera_position = (1, -1, 1) # +X, +Z, -Y
219
+
220
+ def _update_camera(self):
221
+ x,y,z = self._plot.camera.position
222
+ d = (x**2+y**2+z**2)**(0.5)
223
+ px, py, pz = self.camera_position
224
+ dp = (px**2+py**2+pz**2)**(0.5)
225
+ px, py, pz = px/dp, py/dp, pz/dp
226
+ self._plot.camera.position = (d*px, d*py, d*pz)
227
+
218
228
  def activate_ruler(self):
219
229
  self._plot.disable_picking()
220
230
  self._selector.turn_off()
@@ -228,6 +238,7 @@ class PVDisplay(BaseDisplay):
228
238
  def show(self):
229
239
  """ Shows the Pyvista display. """
230
240
  self._ruler.min_length = max(1e-3, min(self._mesh.edge_lengths))
241
+ self._update_camera()
231
242
  self._add_aux_items()
232
243
  if self._do_animate:
233
244
  self._plot.show(auto_close=False, interactive_update=True, before_close_callback=self._close_callback)
@@ -300,7 +311,25 @@ class PVDisplay(BaseDisplay):
300
311
  cells[:,1:] = self._mesh.tets[:,tets].T
301
312
  cells[:,0] = 4
302
313
  celltypes = np.full(ntets, fill_value=pv.CellType.TETRA, dtype=np.uint8)
303
- points = self._mesh.nodes.T
314
+ points = self._mesh.nodes.copy().T
315
+ return pv.UnstructuredGrid(cells, celltypes, points)
316
+
317
+ def _volume_edges(self, obj: GeoObject | Selection) -> pv.UnstructuredGrid:
318
+ """Adds the edges of objects
319
+
320
+ Args:
321
+ obj (DomainSelection | None, optional): _description_. Defaults to None.
322
+
323
+ Returns:
324
+ pv.UnstructuredGrid: The unstrutured grid object
325
+ """
326
+ edge_ids = self._mesh.domain_edges(obj.dimtags)
327
+ nedges = edge_ids.shape[0]
328
+ cells = np.zeros((nedges,3), dtype=np.int64)
329
+ cells[:,1:] = self._mesh.edges[:,edge_ids].T
330
+ cells[:,0] = 2
331
+ celltypes = np.full(nedges, fill_value=pv.CellType.CUBIC_LINE, dtype=np.uint8)
332
+ points = self._mesh.nodes.copy().T
304
333
  return pv.UnstructuredGrid(cells, celltypes, points)
305
334
 
306
335
  def mesh_surface(self, surface: FaceSelection) -> pv.UnstructuredGrid:
@@ -310,7 +339,8 @@ class PVDisplay(BaseDisplay):
310
339
  cells[:,1:] = self._mesh.tris[:,tris].T
311
340
  cells[:,0] = 3
312
341
  celltypes = np.full(ntris, fill_value=pv.CellType.TRIANGLE, dtype=np.uint8)
313
- points = self._mesh.nodes.T
342
+ points = self._mesh.nodes.copy().T
343
+ points[:,2] += self.set.z_boost
314
344
  return pv.UnstructuredGrid(cells, celltypes, points)
315
345
 
316
346
  def mesh(self, obj: GeoObject | Selection | Iterable) -> pv.UnstructuredGrid | None:
@@ -328,8 +358,10 @@ class PVDisplay(BaseDisplay):
328
358
 
329
359
  ## OBLIGATORY METHODS
330
360
  def add_object(self, obj: GeoObject | Selection, *args, **kwargs):
331
- kwargs = setdefault(kwargs, color=obj.color_rgb, opacity=obj.opacity, silhouette=True, pickable=True)
332
- self._plot.add_mesh(self.mesh(obj), *args, **kwargs)
361
+ kwargs = setdefault(kwargs, color=obj.color_rgb, opacity=obj.opacity, silhouette=False, show_edges=False, pickable=True)
362
+
363
+ actor = self._plot.add_mesh(self.mesh(obj), *args, **kwargs)
364
+ self._plot.add_mesh(self._volume_edges(_select(obj)), color='#000000', line_width=1, show_edges=True)
333
365
 
334
366
  def add_scatter(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray):
335
367
  """Adds a scatter point cloud
@@ -348,7 +380,7 @@ class PVDisplay(BaseDisplay):
348
380
  XYZ=None,
349
381
  field: Literal['E','H'] = 'E',
350
382
  k0: float | None = None,
351
- mode_number: int | None = None) -> None:
383
+ mode_number: int = 0) -> None:
352
384
 
353
385
  if XYZ:
354
386
  X,Y,Z = XYZ
@@ -385,6 +417,7 @@ class PVDisplay(BaseDisplay):
385
417
  k0 = port.get_mode(0).k0
386
418
  else:
387
419
  k0 = 1
420
+ port.selected_mode = mode_number
388
421
  F = port.port_mode_3d_global(xf,yf,zf,k0, which=field)
389
422
 
390
423
  Fx = F[0,:].reshape(X.shape).T
@@ -22,3 +22,4 @@ class PVDisplaySettings:
22
22
  self.background_bottom: str = "#c0d2e8"
23
23
  self.background_top: str = "#ffffff"
24
24
  self.grid_line_color: str = "#8e8e8e"
25
+ self.z_boost: float = 1e-6