emerge 1.0.7__py3-none-any.whl → 1.1.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (33) hide show
  1. emerge/__init__.py +15 -3
  2. emerge/_emerge/const.py +2 -1
  3. emerge/_emerge/elements/ned2_interp.py +122 -42
  4. emerge/_emerge/geo/__init__.py +1 -1
  5. emerge/_emerge/geo/operations.py +20 -0
  6. emerge/_emerge/geo/pcb.py +162 -71
  7. emerge/_emerge/geo/shapes.py +12 -7
  8. emerge/_emerge/geo/step.py +177 -41
  9. emerge/_emerge/geometry.py +189 -27
  10. emerge/_emerge/logsettings.py +26 -2
  11. emerge/_emerge/material.py +2 -0
  12. emerge/_emerge/mesh3d.py +6 -8
  13. emerge/_emerge/mesher.py +67 -11
  14. emerge/_emerge/mth/common_functions.py +1 -1
  15. emerge/_emerge/mth/optimized.py +2 -2
  16. emerge/_emerge/physics/microwave/adaptive_mesh.py +549 -116
  17. emerge/_emerge/physics/microwave/assembly/assembler.py +9 -1
  18. emerge/_emerge/physics/microwave/microwave_3d.py +133 -83
  19. emerge/_emerge/physics/microwave/microwave_bc.py +158 -8
  20. emerge/_emerge/physics/microwave/microwave_data.py +94 -5
  21. emerge/_emerge/plot/pyvista/display.py +36 -23
  22. emerge/_emerge/selection.py +17 -2
  23. emerge/_emerge/settings.py +124 -6
  24. emerge/_emerge/simmodel.py +273 -150
  25. emerge/_emerge/simstate.py +106 -0
  26. emerge/_emerge/simulation_data.py +11 -23
  27. emerge/_emerge/solve_interfaces/cudss_interface.py +20 -1
  28. emerge/_emerge/solver.py +4 -4
  29. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/METADATA +7 -3
  30. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/RECORD +33 -32
  31. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/WHEEL +0 -0
  32. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/entry_points.txt +0 -0
  33. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -273,13 +273,17 @@ class Assembler:
273
273
 
274
274
  logger.debug('Implementing PEC Boundary Conditions.')
275
275
  pec_ids: list[int] = []
276
+ pec_tris: list[int] = []
276
277
 
277
278
  # Conductivity above al imit, consider it all PEC
278
279
  ipec = 0
280
+
279
281
  for itet in range(field.n_tets):
280
282
  if cond[0,0,itet] > self.settings.mw_3d_peclim:
281
283
  ipec+=1
282
284
  pec_ids.extend(field.tet_to_field[:,itet])
285
+ for tri in field.mesh.tet_to_tri[:,itet]:
286
+ pec_tris.append(tri)
283
287
  if ipec>0:
284
288
  logger.trace(f'Extended PEC with {ipec} tets with a conductivity > {self.settings.mw_3d_peclim}.')
285
289
 
@@ -295,9 +299,12 @@ class Assembler:
295
299
  eids = field.edge_to_field[:, ii]
296
300
  pec_ids.extend(list(eids))
297
301
 
302
+
298
303
  for ii in tri_ids:
299
304
  tids = field.tri_to_field[:, ii]
300
305
  pec_ids.extend(list(tids))
306
+
307
+ pec_tris.extend(tri_ids)
301
308
 
302
309
 
303
310
  ############################################################
@@ -412,7 +419,8 @@ class Assembler:
412
419
 
413
420
  simjob.port_vectors = port_vectors
414
421
  simjob.solve_ids = solve_ids
415
-
422
+ simjob._pec_tris = pec_tris
423
+
416
424
  if has_periodic:
417
425
  simjob.P = Pmat
418
426
  simjob.Pd = Pmat.getH()
@@ -19,13 +19,16 @@ from ...mesher import Mesher
19
19
  from ...material import Material
20
20
  from ...mesh3d import Mesh3D
21
21
  from ...coord import Line
22
- from ...geometry import GeoSurface, _GEOMANAGER
22
+ from ...geometry import GeoSurface
23
23
  from ...elements.femdata import FEMBasis
24
24
  from ...elements.nedelec2 import Nedelec2
25
25
  from ...solver import DEFAULT_ROUTINE, SolveRoutine
26
26
  from ...system import called_from_main_function
27
27
  from ...selection import FaceSelection
28
28
  from ...settings import Settings
29
+ from ...simstate import SimState
30
+ from ...logsettings import DEBUG_COLLECTOR
31
+
29
32
  from .microwave_bc import MWBoundaryConditionSet, PEC, ModalPort, LumpedPort, PortBC
30
33
  from .microwave_data import MWData
31
34
  from .assembly.assembler import Assembler
@@ -34,10 +37,10 @@ from .simjob import SimJob
34
37
 
35
38
  from concurrent.futures import ThreadPoolExecutor
36
39
  from loguru import logger
37
- from typing import Callable, Literal
40
+ from typing import Callable, Literal, Any
38
41
  import multiprocessing as mp
39
42
  from cmath import sqrt as csqrt
40
-
43
+ from itertools import product
41
44
  import numpy as np
42
45
  import threading
43
46
  import time
@@ -54,16 +57,14 @@ def run_job_multi(job: SimJob) -> SimJob:
54
57
  Returns:
55
58
  SimJob: The solved SimJob
56
59
  """
57
- routine = DEFAULT_ROUTINE._configure_routine('MP')
60
+ nr = int(mp.current_process().name.split('-')[1])
61
+ routine = DEFAULT_ROUTINE._configure_routine('MP', proc_nr=nr)
58
62
  for A, b, ids, reuse, aux in job.iter_Ab():
59
63
  solution, report = routine.solve(A, b, ids, reuse, id=job.id)
60
64
  report.add(**aux)
61
65
  job.submit_solution(solution, report)
62
66
  return job
63
67
 
64
- def _init_worker():
65
- nr = int(mp.current_process().name.split('-')[1])
66
- DEFAULT_ROUTINE._configure_routine(proc_nr=nr)
67
68
 
68
69
  def _dimstring(data: list[float] | np.ndarray) -> str:
69
70
  """A String formatter for dimensions in millimeters
@@ -111,14 +112,17 @@ class Microwave3D:
111
112
  formulation.
112
113
 
113
114
  """
114
- def __init__(self, mesher: Mesher, settings: Settings, mwdata: MWData, order: int = 2):
115
+ def __init__(self, state: SimState, mesher: Mesher, settings: Settings, order: int = 2):
116
+
117
+ self._settings: Settings = settings
118
+
115
119
  self.frequencies: list[float] = []
116
120
  self.current_frequency = 0
117
121
  self.order: int = order
118
- self.resolution: float = 1
119
- self._settings: Settings = settings
122
+ self.resolution: float = 0.33
123
+
120
124
  self.mesher: Mesher = mesher
121
- self.mesh: Mesh3D = Mesh3D(self.mesher)
125
+ self._state: SimState = state
122
126
 
123
127
  self.assembler: Assembler = Assembler(self._settings)
124
128
  self.bc: MWBoundaryConditionSet = MWBoundaryConditionSet(None)
@@ -128,41 +132,34 @@ class Microwave3D:
128
132
 
129
133
  ## States
130
134
  self._bc_initialized: bool = False
131
- self.data: MWData = mwdata
132
-
133
- ## Data
134
- self._params: dict[str, float] = dict()
135
135
  self._simstart: float = 0.0
136
136
  self._simend: float = 0.0
137
+
138
+ self._container: dict[str, Any] = dict()
137
139
 
138
- self.set_order(order)
139
-
140
- def reset_data(self):
141
- self.data = MWData()
142
-
140
+ @property
141
+ def _params(self) -> dict[str, float]:
142
+ return self._state.params
143
+
144
+ @property
145
+ def mesh(self) -> Mesh3D:
146
+ return self._state.mesh
147
+
148
+ @property
149
+ def data(self) -> MWData:
150
+ return self._state.data.mw
151
+
143
152
  def reset(self, _reset_bc: bool = True):
144
153
  if _reset_bc:
145
- self.bc.reset()
146
154
  self.bc = MWBoundaryConditionSet(None)
155
+ else:
156
+ for bc in self.bc.oftype(ModalPort):
157
+ bc.reset()
158
+
147
159
  self.basis: FEMBasis = None
148
160
  self.solveroutine.reset()
149
161
  self.assembler.cached_matrices = None
150
162
 
151
- def set_order(self, order: int) -> None:
152
- """Sets the order of the basis functions used. Currently only supports second order.
153
-
154
- Args:
155
- order (int): The order to use.
156
-
157
- Raises:
158
- ValueError: An error if a wrong order is used.
159
- """
160
- if order not in (2,):
161
- raise ValueError(f'Order {order} not supported. Only order-2 allowed.')
162
-
163
- self.order = order
164
- self.resolution = {1: 0.15, 2: 0.3}[order]
165
-
166
163
  @property
167
164
  def nports(self) -> int:
168
165
  """The number of ports in the physics.
@@ -219,7 +216,15 @@ class Microwave3D:
219
216
  self.frequencies = list(frequency)
220
217
  else:
221
218
  self.frequencies = [frequency]
222
-
219
+
220
+ # Safety tests
221
+ if len(self.frequencies) > 200:
222
+ DEBUG_COLLECTOR.add_report(f'More than 200 frequency points are detected ({len(frequency)}). This may cause slow simulations. Consider using Vector Fitting to subsample S-parameters.')
223
+ if min(self.frequencies) < 1e6:
224
+ DEBUG_COLLECTOR.add_report(f'A frequency smaller than 1MHz has been detected ({min(frequency)} Hz). Perhaps you forgot to include usints like 1e6 for MHz etc.')
225
+ if max(self.frequencies) > 1e12:
226
+ DEBUG_COLLECTOR.add_report(f'A frequency greater than THz has been detected ({min(frequency)} Hz). Perhaps you double counted frequency units like twice 1e6 for MHz etc.')
227
+
223
228
  self.mesher.max_size = self.resolution * 299792458 / max(self.frequencies)
224
229
  self.mesher.min_size = 0.1 * self.mesher.max_size
225
230
 
@@ -361,29 +366,6 @@ class Microwave3D:
361
366
  logger.trace(f' - Port[{port.port_number}] integration line {start} -> {end}.')
362
367
 
363
368
  port.v_integration = True
364
-
365
- def _compute_integration_line(self, group1: list[int], group2: list[int]) -> tuple[np.ndarray, np.ndarray]:
366
- """Computes an integration line for two node island groups by finding the closest two nodes.
367
-
368
- This method is used for the modal TEM analysis to find an appropriate voltage integration path
369
- by looking for the two closest points for the two conductor islands that where discovered.
370
-
371
- Currently it defaults to 11 integration line points.
372
-
373
- Args:
374
- group1 (list[int]): The first island node group
375
- group2 (list[int]): The second island node group
376
-
377
- Returns:
378
- centers (np.ndarray): The center points of the line segments
379
- dls (np.ndarray): The delta-path vectors for each line segment.
380
- """
381
- nodes1 = self.mesh.nodes[:,group1]
382
- nodes2 = self.mesh.nodes[:,group2]
383
- path = shortest_path(nodes1, nodes2, 21)
384
- centres = (path[:,1:] + path[:,:-1])/2
385
- dls = path[:,1:] - path[:,:-1]
386
- return centres, dls
387
369
 
388
370
  def _find_tem_conductors(self, port: ModalPort, sigtri: np.ndarray) -> tuple[list[int], list[int]]:
389
371
  ''' Returns two lists of global node indices corresponding to the TEM port conductors.
@@ -449,7 +431,8 @@ class Microwave3D:
449
431
  min_term = i
450
432
 
451
433
  if plus_term is None or min_term is None:
452
- raise ValueError(f' - Found {len(pec_islands)} PEC islands without a terminal definition. Please use .set_terminals() to define which conductors are which polarity.')
434
+ logger.error(f' - Found {len(pec_islands)} PEC islands without a terminal definition. Please use .set_terminals() to define which conductors are which polarity, or define the integration line manually.')
435
+ return None, None
453
436
  logger.debug(f'Positive island = {pec_island_tags[plus_term]}')
454
437
  logger.debug(f'Negative island = {pec_island_tags[min_term]}')
455
438
  pec_islands = [pec_islands[plus_term], pec_islands[min_term]]
@@ -479,7 +462,11 @@ class Microwave3D:
479
462
  if not bc.mixed_materials and bc.initialized:
480
463
  continue
481
464
 
482
- self.modal_analysis(bc, 1, False, bc.TEM, freq=freq)
465
+ if bc.forced_modetype=='TEM':
466
+ TEM = True
467
+ else:
468
+ TEM = False
469
+ self.modal_analysis(bc, 1, direct=False, freq=freq, TEM=TEM)
483
470
 
484
471
  def modal_analysis(self,
485
472
  port: ModalPort,
@@ -573,7 +560,7 @@ class Microwave3D:
573
560
  target_kz = k0*target_neff
574
561
 
575
562
  if target_kz is None:
576
- if TEM:
563
+ if TEM or port.forced_modetype=='TEM':
577
564
  target_kz = ermean*urmean*1.1*k0
578
565
  else:
579
566
 
@@ -595,19 +582,28 @@ class Microwave3D:
595
582
  Emode = np.zeros((nlf.n_field,), dtype=np.complex128)
596
583
  eigenmode = eigen_modes[:,i]
597
584
  Emode[solve_ids] = np.squeeze(eigenmode)
598
- Emode = Emode * np.exp(-1j*np.angle(np.max(Emode)))
585
+ Emode = Emode * np.exp(-1j*np.angle(Emode[np.argmax(np.abs(Emode))]))
599
586
 
600
587
  beta_base = np.emath.sqrt(-eigen_values[i])
601
588
  beta = min(k0*np.sqrt(ermax*urmax), beta_base)
602
589
 
603
590
  residuals = -1
604
591
 
592
+ if port._get_alignment_vector(i) is not None:
593
+ vec = port._get_alignment_vector(i)
594
+ xyz_centers = self.mesh.tri_centers[:,self.mesh.get_triangles(port.tags)]
595
+ E_centers = np.mean(nlf.interpolate_Ef(Emode)(xyz_centers[0,:], xyz_centers[1,:], xyz_centers[2,:]), axis=1)
596
+ EdotVec = vec[0]*E_centers[0] + vec[1]*E_centers[1] + vec[2]*E_centers[2]
597
+ if EdotVec.real < 0:
598
+ logger.debug(f'Mode polarization along alignment axis {vec} = {EdotVec.real:.3f}, inverting.')
599
+ Emode = -Emode
600
+
605
601
  portfE = nlf.interpolate_Ef(Emode)
606
602
  portfH = nlf.interpolate_Hf(Emode, k0, ur, beta)
607
-
608
603
  P = compute_avg_power_flux(nlf, Emode, k0, ur, beta)
609
604
 
610
- mode = port.add_mode(Emode, portfE, portfH, beta, k0, residuals, TEM=TEM, freq=freq)
605
+ mode = port.add_mode(Emode, portfE, portfH, beta, k0, residuals, number=i, freq=freq)
606
+
611
607
  if mode is None:
612
608
  continue
613
609
 
@@ -616,21 +612,40 @@ class Microwave3D:
616
612
  Ez = np.max(np.abs(Efz))
617
613
  Exy = np.max(np.abs(Efxy))
618
614
 
619
- if Ez/Exy < 1e-1 and not TEM:
615
+ if port.forced_modetype == 'TEM' or TEM:
616
+ mode.modetype = 'TEM'
617
+
618
+ if len(port.vintline)>0:
619
+ line = port.vintline[0]
620
+ else:
621
+ G1, G2 = self._find_tem_conductors(port, sigtri=cond)
622
+ if G1 is None or G2 is None:
623
+ logger.warning('Skipping characteristic impedance calculation.')
624
+ continue
625
+
626
+ nodes1 = self.mesh.nodes[:,G1]
627
+ nodes2 = self.mesh.nodes[:,G2]
628
+ path = shortest_path(nodes1, nodes2, 2)
629
+ line = Line.from_points(path[:,0], path[:,1], 21)
630
+ port.vintline.append(line)
631
+
632
+ cs = np.array(line.cmid)
633
+
634
+ logger.debug(f'Integrating portmode from {cs[:,0]} to {cs[:,-1]}')
635
+ voltage = line.line_integral(portfE)
636
+ # Align mode polarity to positive voltage
637
+ if voltage < 0:
638
+ mode.polarity = mode.polarity * -1
639
+
640
+ mode.Z0 = abs(voltage**2/(2*P))
641
+ logger.debug(f'Port Z0 = {mode.Z0}')
642
+ elif Ez/Exy < 1e-1 or port.forced_modetype=='TE':
620
643
  logger.debug('Low Ez/Et ratio detected, assuming TE mode')
621
644
  mode.modetype = 'TE'
622
- elif Ez/Exy > 1e-1 and not TEM:
645
+ elif Ez/Exy > 1e-1 or port.forced_modetype=='TM':
623
646
  logger.debug('High Ez/Et ratio detected, assuming TM mode')
624
647
  mode.modetype = 'TM'
625
- elif TEM:
626
- G1, G2 = self._find_tem_conductors(port, sigtri=cond)
627
- cs, dls = self._compute_integration_line(G1,G2)
628
- logger.debug(f'Integrating portmode from {cs[:,0]} to {cs[:,-1]}')
629
- mode.modetype = 'TEM'
630
- Ex, Ey, Ez = portfE(cs[0,:], cs[1,:], cs[2,:])
631
- voltage = np.sum(Ex*dls[0,:] + Ey*dls[1,:] + Ez*dls[2,:])
632
- mode.Z0 = abs(voltage**2/(2*P))
633
- logger.debug(f'Port Z0 = {mode.Z0}')
648
+
634
649
 
635
650
  mode.set_power(P*port._qmode(k0)**2)
636
651
 
@@ -810,7 +825,7 @@ class Microwave3D:
810
825
  "if __name__ == '__main__' guard in the top-level script."
811
826
  )
812
827
  # Start parallel pool
813
- with mp.Pool(processes=n_workers, initializer=_init_worker) as pool:
828
+ with mp.Pool(processes=n_workers) as pool:
814
829
  for i_group, fgroup in enumerate(freq_groups):
815
830
  logger.debug(f'Precomputing group {i_group}.')
816
831
  jobs = []
@@ -861,7 +876,7 @@ class Microwave3D:
861
876
  def _run_adaptive_mesh(self,
862
877
  iteration: int,
863
878
  frequency: float,
864
- automatic_modal_analysis: bool = True) -> MWData:
879
+ automatic_modal_analysis: bool = True) -> tuple[MWData, list[int]]:
865
880
  """Executes a frequency domain study
866
881
 
867
882
  The study is distributed over "n_workers" workers.
@@ -950,7 +965,7 @@ class Microwave3D:
950
965
  self.solveroutine.reset()
951
966
  ### Compute S-parameters and return
952
967
  self._post_process([job,], [mats,])
953
- return self.data
968
+ return self.data, job._pec_tris
954
969
 
955
970
  def eigenmode(self, search_frequency: float,
956
971
  nmodes: int = 6,
@@ -1045,14 +1060,21 @@ class Microwave3D:
1045
1060
  """
1046
1061
  if self.basis is None:
1047
1062
  raise SimulationError('Cannot post-process. Simulation basis function is undefined.')
1063
+
1048
1064
  mesh = self.mesh
1049
1065
  all_ports = self.bc.oftype(PortBC)
1050
1066
  port_numbers = [port.port_number for port in all_ports]
1051
1067
 
1052
1068
  logger.info('Computing S-parameters')
1053
1069
 
1054
-
1055
- for freq, job, mats in zip(self.frequencies, results, materials):
1070
+ not_conserved = False
1071
+
1072
+ single_corr = self._settings.mw_cap_sp_single
1073
+ col_corr = self._settings.mw_cap_sp_col
1074
+ recip_corr = self._settings.mw_recip_sp
1075
+
1076
+ for job, mats in zip(results, materials):
1077
+ freq = job.freq
1056
1078
  er, ur, cond = mats
1057
1079
  ertri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
1058
1080
  urtri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
@@ -1078,6 +1100,7 @@ class Microwave3D:
1078
1100
 
1079
1101
  logger.info(f'Post Processing simulation frequency = {freq/1e9:.3f} GHz')
1080
1102
 
1103
+
1081
1104
  # Recording port information
1082
1105
  for active_port in all_ports:
1083
1106
  port_tets = self.mesh.get_face_tets(active_port.tags)
@@ -1123,10 +1146,36 @@ class Microwave3D:
1123
1146
  pfield, pmode = self._compute_s_data(bc, fieldf,tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
1124
1147
  logger.debug(f'[{bc.port_number}] Passive amplitude = {np.abs(pfield):.3f}')
1125
1148
  scalardata.write_S(bc.port_number, active_port.port_number, pfield/Pout)
1149
+ if abs(pfield/Pout) > 1.0:
1150
+ logger.warning(f'S-parameter > 1.0 detected: {np.abs(pfield/Pout)}')
1151
+ not_conserved = True
1126
1152
  active_port.active=False
1127
1153
 
1154
+
1128
1155
  fielddata.set_field_vector()
1156
+
1157
+ N = scalardata.Sp.shape[1]
1158
+
1159
+ # Enforce reciprocity
1160
+ if recip_corr:
1161
+ scalardata.Sp = (scalardata.Sp + scalardata.Sp.T)/2
1162
+
1163
+ # Enforce energy conservation
1164
+ if col_corr:
1165
+ for j in range(N):
1166
+ scalardata.Sp[:,j] = scalardata.Sp[:,j] / max(1.0, np.sum(np.abs(scalardata.Sp[:,j])**2))
1167
+
1168
+ # Enforce S-parameter limit to 1.0
1169
+ if single_corr:
1170
+ for i,j in product(range(N), range(N)):
1171
+ scalardata.Sp[i,j] = scalardata.Sp[i,j] / max(1.0, np.abs(scalardata.Sp[i,j]))
1172
+
1173
+
1129
1174
 
1175
+ if not_conserved:
1176
+ DEBUG_COLLECTOR.add_report('S-parameters with an amplitude greater than 1.0 detected. This could be due to a ModalPort with the wrong mode type.\n' +
1177
+ 'Specify the type of mode (TE/TM/TEM) in the constructor using ModalPort(..., modetype=\'TE\') for example.\n' +
1178
+ f'Values slightly greater than 1 are possible due to numerical accuracy. Automatic normalization = {single_corr or col_corr}')
1130
1179
  logger.info('Simulation Complete!')
1131
1180
  self._simend = time.time()
1132
1181
  logger.info(f'Elapsed time = {(self._simend-self._simstart):.2f} seconds.')
@@ -1152,6 +1201,7 @@ class Microwave3D:
1152
1201
  tuple[complex, complex]: _description_
1153
1202
  """
1154
1203
  from .sparam import sparam_field_power, sparam_mode_power
1204
+
1155
1205
  if bc.v_integration:
1156
1206
  if bc.vintline is None:
1157
1207
  raise SimulationError('Trying to compute characteristic impedance but no integration line is defined.')
@@ -1177,7 +1227,7 @@ class Microwave3D:
1177
1227
  else:
1178
1228
  if bc.modetype(k0) == 'TEM':
1179
1229
  const = 1/(np.sqrt((urp[0,0,:] + urp[1,1,:] + urp[2,2,:])/(erp[0,0,:] + erp[1,1,:] + erp[2,2,:])))
1180
- if bc.modetype(k0) == 'TE':
1230
+ elif bc.modetype(k0) == 'TE':
1181
1231
  const = 1/((urp[0,0,:] + urp[1,1,:] + urp[2,2,:])/3)
1182
1232
  elif bc.modetype(k0) == 'TM':
1183
1233
  const = 1/((erp[0,0,:] + erp[1,1,:] + erp[2,2,:])/3)