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
emerge/_emerge/geo/pcb.py CHANGED
@@ -25,7 +25,7 @@ from .polybased import XYPolygon
25
25
  from .operations import change_coordinate_system, unite
26
26
  from .pcb_tools.macro import parse_macro
27
27
  from .pcb_tools.calculator import PCBCalculator
28
-
28
+ from ..logsettings import DEBUG_COLLECTOR
29
29
  import numpy as np
30
30
  from loguru import logger
31
31
  from typing import Literal, Callable, overload
@@ -934,12 +934,14 @@ class StripPath:
934
934
  return self.path[element_nr]
935
935
 
936
936
  class PCBLayer:
937
-
937
+ _DEFNAME: str = 'PCBLayer'
938
938
  def __init__(self,
939
939
  thickness: float,
940
- material: Material):
940
+ material: Material,
941
+ name: str | None = None):
941
942
  self.th: float = thickness
942
943
  self.mat: Material = material
944
+ self.name: str = _NAME_MANAGER(name, self._DEFNAME)
943
945
 
944
946
  ############################################################
945
947
  # PCB DESIGN CLASS #
@@ -958,7 +960,8 @@ class PCB:
958
960
  stack: list[PCBLayer] = None,
959
961
  name: str | None = None,
960
962
  trace_thickness: float | None = None,
961
- zs: np.ndarray | None = None
963
+ zs: np.ndarray | None = None,
964
+ thick_traces: bool = False,
962
965
  ):
963
966
  """Creates a new PCB layout class instance
964
967
 
@@ -972,10 +975,13 @@ class PCB:
972
975
  stack (list[PCBLayer], optional): Optional list of PCBLayer classes for multilayer PCB with different dielectrics. Defaults to None.
973
976
  name (str | None, optional): The PCB object name. Defaults to None.
974
977
  trace_thickness (float | None, optional): The conductor trace thickness if important. Defaults to None.
978
+ thick_traces: (bool, optional): If traces should be given a thickness and modeled in 3D. Defaults to False
975
979
  """
976
980
 
977
981
  self.thickness: float = thickness
982
+ self._thick_traces: bool = thick_traces
978
983
  self._stack: list[PCBLayer] = []
984
+
979
985
  if zs is not None:
980
986
  self._zs = zs
981
987
  self.thickness = np.max(zs)-np.min(zs)
@@ -1013,9 +1019,10 @@ class PCB:
1013
1019
 
1014
1020
  self.dielectric_priority: int = 11
1015
1021
  self.via_priority: int = 12
1022
+ self.conductor_priority: int = 13
1016
1023
 
1017
- self.traces: list[GeoPolygon] = []
1018
- self.ports: list[GeoPolygon] = []
1024
+ self.traces: list[GeoPolygon | GeoVolume] = []
1025
+ self.ports: list[GeoPolygon | GeoVolume] = []
1019
1026
  self.vias: list[Via] = []
1020
1027
 
1021
1028
  self.xs: list[float] = []
@@ -1029,9 +1036,18 @@ class PCB:
1029
1036
  self.calc: PCBCalculator = PCBCalculator(self.thickness, self._zs, self.material, self.unit)
1030
1037
 
1031
1038
  self.name: str = _NAME_MANAGER(name, self._DEFNAME)
1032
-
1039
+
1040
+ if not self._thick_traces and self.trace_material is not PEC:
1041
+ DEBUG_COLLECTOR.add_report('Non PEC surface materials are used without thick traces. The SurfaceImpedance boundary condition will be used that is known to not be accurate.' +
1042
+ 'Please set thick_traces=True in the PCB constructor to ensure accurate losses until this issue is fixed.')
1043
+
1044
+ ############################################################
1045
+ # PROPERTIES #
1046
+ ############################################################
1047
+
1033
1048
  @property
1034
1049
  def trace(self) -> GeoPolygon:
1050
+ ""
1035
1051
  tags = []
1036
1052
  for trace in self.traces:
1037
1053
  tags.extend(trace.tags)
@@ -1040,29 +1056,36 @@ class PCB:
1040
1056
 
1041
1057
  @property
1042
1058
  def all_objects(self) -> list[GeoPolygon]:
1043
- return self.traces + self.ports
1044
-
1045
-
1046
- def z(self, layer: int) -> float:
1047
- """Returns the z-height of the given layer number counter from 1 (bottom) to N (top)
1048
-
1049
- Args:
1050
- layer (int): The layer number (1 to N)
1059
+ """Returns all objects gnerated by the PCB layer.
1051
1060
 
1052
1061
  Returns:
1053
- float: the z-height
1062
+ list[GeoPolygon]: _description_
1054
1063
  """
1055
- if layer <= 0:
1056
- return self._zs[layer]
1057
- return self._zs[layer-1]
1064
+ return self.traces + self.ports
1058
1065
 
1059
1066
  @property
1060
1067
  def top(self) -> float:
1068
+ """ The top conductor later height (z-value in meters)."""
1061
1069
  return self._zs[-1]
1062
1070
 
1063
1071
  @property
1064
1072
  def bottom(self) -> float:
1073
+ """The bottom conductor layer height (z-value in meters)
1074
+
1075
+ """
1065
1076
  return self._zs[0]
1077
+
1078
+
1079
+ ############################################################
1080
+ # PRIVATE FUNCTIONS #
1081
+ ############################################################
1082
+
1083
+ def _lumped_element(self, poly: XYPolygon, function: Callable, width: float, length: float, name: str | None = 'LumpedElement') -> None:
1084
+ geopoly = poly._finalize(self.cs, name=name)
1085
+ geopoly._aux_data['func'] = function
1086
+ geopoly._aux_data['width'] = width
1087
+ geopoly._aux_data['height'] = length
1088
+ self.lumped_elements.append(geopoly)
1066
1089
 
1067
1090
  def _get_z(self, element: RouteElement) -> float :
1068
1091
  """Return the z-height of a given Route Element
@@ -1077,12 +1100,70 @@ class PCB:
1077
1100
  if path._has(element):
1078
1101
  return path.z
1079
1102
  raise RouteException('Requesting z-height of route element that is not contained in a path.')
1103
+
1104
+ def __call__(self, path_nr: int) -> StripPath:
1105
+ if path_nr >= len(self.paths):
1106
+ self.paths.append(StripPath(self))
1107
+ return self.paths[path_nr]
1108
+
1109
+ def _gen_poly(self, xys: list[tuple[float, float]], z: float, name: str | None = None) -> GeoPolygon | GeoVolume:
1110
+ """ Generates a GeoPoly out of a list of (x,y) coordinate tuples.
1111
+
1112
+
1113
+ Args:
1114
+ xys (list[tuple[float, float]]): A list of (x,y) coordinate tuples.
1115
+ z (float, optional): The z-height of the polygon. Defaults to the top layer.
1116
+ name (str, optional): The name of the polygon.
1117
+ """
1118
+ ptags = []
1119
+ for x,y in xys:
1120
+ px, py, pz = self.cs.in_global_cs(x*self.unit, y*self.unit, z*self.unit)
1121
+ ptags.append(gmsh.model.occ.addPoint(px, py, pz))
1122
+
1123
+ ltags = []
1124
+ for t1, t2 in zip(ptags[:-1], ptags[1:]):
1125
+ ltags.append(gmsh.model.occ.addLine(t1, t2))
1126
+ ltags.append(gmsh.model.occ.addLine(ptags[-1], ptags[0]))
1127
+
1128
+ tag_wire = gmsh.model.occ.addWire(ltags)
1129
+ planetag = gmsh.model.occ.addPlaneSurface([tag_wire,])
1130
+ if self._thick_traces:
1131
+ if self.trace_thickness is None:
1132
+ raise ValueError('Trace thickness not defined, cannot generate polygons. Make sure to define a trace thickness in the PCB() constructor.')
1133
+ dx, dy, dz = self.cs.zax.np*self.trace_thickness
1134
+ dimtags = gmsh.model.occ.extrude([(2,planetag),], dx, dy, dz)
1135
+ voltags = [dt[1] for dt in dimtags if dt[0]==3]
1136
+ poly = GeoVolume(voltags, name=name).prio_set(self.conductor_priority)
1137
+ else:
1138
+ poly = GeoPolygon([planetag,], name=name)
1139
+ poly._store('thickness', self.trace_thickness)
1140
+ return poly
1141
+
1142
+
1143
+ ############################################################
1144
+ # USER FUNCTIONS #
1145
+ ############################################################
1146
+
1147
+ def z(self, layer: int) -> float:
1148
+ """Returns the z-height of the given layer number counter from 1 (bottom) to N (top)
1149
+
1150
+ Args:
1151
+ layer (int): The layer number (1 to N)
1152
+
1153
+ Returns:
1154
+ float: the z-height
1155
+ """
1156
+ if layer <= 0:
1157
+ return self._zs[layer]
1158
+ return self._zs[layer-1]
1080
1159
 
1081
1160
  def add_vias(self, *coordinates: tuple[float, float], radius: float,
1082
1161
  z1: float | None = None,
1083
1162
  z2: float | None = None,
1084
1163
  segments: int = 6) -> None:
1085
- """Add a series of vias provided by a list of coordinates.
1164
+ """Add a series of vias provided by a list of coordinates.
1165
+
1166
+ Vias will not be created yet. To generate the actual geometries use the function .generate_vias().
1086
1167
 
1087
1168
  Make sure to define the radius explicitly, otherwise the radius gets interpreted as a coordinate:
1088
1169
 
@@ -1118,10 +1199,7 @@ class PCB:
1118
1199
  return poly
1119
1200
  raise ValueError(f'There is no stripline or coordinate under the name of {name}')
1120
1201
 
1121
- def __call__(self, path_nr: int) -> StripPath:
1122
- if path_nr >= len(self.paths):
1123
- self.paths.append(StripPath(self))
1124
- return self.paths[path_nr]
1202
+
1125
1203
 
1126
1204
  def determine_bounds(self,
1127
1205
  leftmargin: float = 0,
@@ -1173,7 +1251,7 @@ class PCB:
1173
1251
  height: float | None = None,
1174
1252
  origin: tuple[float, float] | None = None,
1175
1253
  alignment: Alignment = Alignment.CORNER,
1176
- name: str | None = None) -> GeoSurface:
1254
+ name: str | None = None) -> GeoSurface | GeoVolume:
1177
1255
  """Generates a generic rectangular plate in the XY grid.
1178
1256
  If no size is provided, it defaults to the entire PCB size assuming that the bounds are determined.
1179
1257
 
@@ -1201,25 +1279,55 @@ class PCB:
1201
1279
  origin[1] - height*self.unit/2,
1202
1280
  origin[2])
1203
1281
 
1204
- plane = Plate(origin, (width*self.unit, 0, 0), (0, height*self.unit, 0), name=name) # type: ignore
1205
- plane._store('thickness', self.thickness)
1206
- plane = change_coordinate_system(plane, self.cs) # type: ignore
1207
- plane.set_material(self.trace_material)
1282
+ if self._thick_traces:
1283
+ plane = Box(width*self.unit, height*self.unit, self.trace_thickness, position=origin, name=name).set_material(self.trace_material)
1284
+ else:
1285
+ plane = Plate(origin, (width*self.unit, 0, 0), (0, height*self.unit, 0), name=name) # type: ignore
1286
+ plane._store('thickness', self.thickness)
1287
+ plane = change_coordinate_system(plane, self.cs) # type: ignore
1288
+ plane.set_material(self.trace_material)
1208
1289
  return plane # type: ignore
1209
1290
 
1291
+ def radial_stub(self, pos: tuple[float, float], length: float, angle: float, direction: tuple[float, float], Nsections: int = 8, w0: float = 0, z: float = 0, material: Material = None, name: str = None) -> None:
1292
+ x0, y0 = pos
1293
+ dx, dy = direction
1294
+
1295
+ rx, ry = dy, -dx
1296
+
1297
+ points = []
1298
+ if w0==0:
1299
+ points.append(pos)
1300
+ else:
1301
+ points.append((x0-rx*w0/2, y0-ry*w0/2))
1302
+ points.append((x0+rx*w0/2, y0+ry*w0/2))
1303
+
1304
+ angs = np.linspace(-angle/2, angle/2, Nsections)*np.pi/180
1305
+ c0 = x0 + 1j*y0
1306
+ vec = length*dx + 1j*length*dy
1307
+ for a in angs:
1308
+ p = c0 + vec*np.exp(1j*a)
1309
+ points.append((p.real, p.imag))
1310
+
1311
+ xs, ys = zip(*points)
1312
+ self.add_poly(xs, ys, z, material, name)
1313
+
1210
1314
  def generate_pcb(self,
1211
1315
  split_z: bool = True,
1212
- merge: bool = True) -> GeoVolume:
1316
+ merge: bool = True) -> list[GeoVolume] | GeoVolume:
1213
1317
  """Generate the PCB Block object
1214
1318
 
1319
+ Args:
1320
+ split_z (bool, optional): If a PCB consisting of a thickness, material and n_layers should be split in sub domains. Defaults to True
1321
+ merge: (bool, optional): If an output list of multiple volumes should be merged into a single object.
1322
+
1215
1323
  Returns:
1216
- GeoVolume: The PCB Block
1324
+ GeoVolume | List[GeoVolume]: The PCB Block or blocks
1217
1325
  """
1218
1326
  x0, y0, z0 = self.origin*self.unit
1219
1327
 
1220
- Nmats = len(set([layer.mat.name for layer in self._stack]))
1328
+ n_materials = len(set([layer.mat.name for layer in self._stack]))
1221
1329
 
1222
- if split_z and self._zs.shape[0]>2 or Nmats > 1:
1330
+ if split_z and self._zs.shape[0]>2 or n_materials > 1:
1223
1331
 
1224
1332
  boxes: list[GeoVolume] = []
1225
1333
  for i, (z1, z2, layer) in enumerate(zip(self._zs[:-1],self._zs[1:],self._stack)):
@@ -1228,12 +1336,12 @@ class PCB:
1228
1336
  self.length*self.unit,
1229
1337
  h*self.unit,
1230
1338
  position=(x0, y0, z0+z1*self.unit),
1231
- name=f'{self.name}_layer{i}')
1339
+ name=layer.name)
1232
1340
  box.material = layer.mat
1233
1341
  box = change_coordinate_system(box, self.cs)
1234
1342
  box.prio_set(self.dielectric_priority)
1235
1343
  boxes.append(box)
1236
- if merge and Nmats == 1:
1344
+ if merge and n_materials == 1:
1237
1345
  return GeoVolume.merged(boxes).prio_set(self.dielectric_priority) # type: ignore
1238
1346
  return boxes # type: ignore
1239
1347
 
@@ -1247,7 +1355,7 @@ class PCB:
1247
1355
  box = change_coordinate_system(box, self.cs)
1248
1356
  return box # type: ignore
1249
1357
 
1250
- def generate_air(self, height: float, name: str = 'PCBAirbox') -> GeoVolume:
1358
+ def generate_air(self, height: float, name: str = 'PCBAirbox', bottom: bool = False) -> GeoVolume:
1251
1359
  """Generate the Air Block object
1252
1360
 
1253
1361
  This requires that the width, depth and origin are deterimed. This
@@ -1256,11 +1364,16 @@ class PCB:
1256
1364
  Returns:
1257
1365
  GeoVolume: The PCB Block
1258
1366
  """
1367
+ dz = 0
1368
+
1259
1369
  x0, y0, z0 = self.origin*self.unit
1370
+ if bottom:
1371
+ dz = z0-self.thickness*self.unit-height*self.unit
1372
+
1260
1373
  box = Box(self.width*self.unit,
1261
1374
  self.length*self.unit,
1262
1375
  height*self.unit,
1263
- position=(x0,y0,z0),
1376
+ position=(x0,y0,z0+dz),
1264
1377
  name=name)
1265
1378
  box = change_coordinate_system(box, self.cs)
1266
1379
  return box # type: ignore
@@ -1334,16 +1447,9 @@ class PCB:
1334
1447
 
1335
1448
  return poly
1336
1449
 
1337
- def _lumped_element(self, poly: XYPolygon, function: Callable, width: float, length: float, name: str | None = 'LumpedElement') -> None:
1338
- geopoly = poly._finalize(self.cs, name=name)
1339
- geopoly._aux_data['func'] = function
1340
- geopoly._aux_data['width'] = width
1341
- geopoly._aux_data['height'] = length
1342
- self.lumped_elements.append(geopoly)
1343
-
1344
1450
  def modal_port(self,
1345
1451
  point: StripLine,
1346
- height: float,
1452
+ height: float | tuple[float, float],
1347
1453
  width_multiplier: float = 5.0,
1348
1454
  width: float | None = None,
1349
1455
  name: str | None = 'ModalPort'
@@ -1362,8 +1468,12 @@ class PCB:
1362
1468
  Returns:
1363
1469
  GeoSurface: The GeoSurface object that can be used for the waveguide.
1364
1470
  """
1365
-
1366
- height = (self.thickness + height)
1471
+ if isinstance(height, tuple):
1472
+ dz, height = height
1473
+ else:
1474
+ dz = 0
1475
+
1476
+ height = (self.thickness + height + dz)
1367
1477
 
1368
1478
  if width is not None:
1369
1479
  W = width
@@ -1373,7 +1483,7 @@ class PCB:
1373
1483
  ds = point.dirright
1374
1484
  x0 = point.x - ds[0]*W/2
1375
1485
  y0 = point.y - ds[1]*W/2
1376
- z0 = - self.thickness
1486
+ z0 = - self.thickness - dz
1377
1487
  ax1 = np.array([ds[0], ds[1], 0])*self.unit*W
1378
1488
  ax2 = np.array([0,0,1])*height*self.unit
1379
1489
 
@@ -1428,36 +1538,17 @@ class PCB:
1428
1538
  """
1429
1539
  if material is None:
1430
1540
  material = self.trace_material
1431
- poly = PCBPoly(xs, ys, z, material,name=name)
1541
+ poly = PCBPoly(xs, ys, z, material, name=name)
1432
1542
 
1433
1543
  self.polies.append(poly)
1434
-
1435
-
1436
- def _gen_poly(self, xys: list[tuple[float, float]], z: float, name: str | None = None) -> GeoPolygon:
1437
- """ Generates a GeoPoly out of a list of (x,y) coordinate tuples"""
1438
- ptags = []
1439
- for x,y in xys:
1440
- px, py, pz = self.cs.in_global_cs(x*self.unit, y*self.unit, z*self.unit)
1441
- ptags.append(gmsh.model.occ.addPoint(px, py, pz))
1442
-
1443
- ltags = []
1444
- for t1, t2 in zip(ptags[:-1], ptags[1:]):
1445
- ltags.append(gmsh.model.occ.addLine(t1, t2))
1446
- ltags.append(gmsh.model.occ.addLine(ptags[-1], ptags[0]))
1447
-
1448
- tag_wire = gmsh.model.occ.addWire(ltags)
1449
- planetag = gmsh.model.occ.addPlaneSurface([tag_wire,])
1450
- poly = GeoPolygon([planetag,], name=name)
1451
- poly._store('thickness', self.trace_thickness)
1452
- return poly
1453
1544
 
1454
1545
  @overload
1455
- def compile_paths(self, merge: Literal[True]) -> GeoSurface: ...
1546
+ def compile_paths(self, merge: Literal[True]) -> GeoSurface | GeoVolume: ...
1456
1547
 
1457
1548
  @overload
1458
- def compile_paths(self, merge: Literal[False] = ...) -> list[GeoSurface]: ...
1549
+ def compile_paths(self, merge: Literal[False] = ...) -> list[GeoSurface] | list[GeoVolume]: ...
1459
1550
 
1460
- def compile_paths(self, merge: bool = False) -> list[GeoPolygon] | GeoSurface:
1551
+ def compile_paths(self, merge: bool = False) -> list[GeoPolygon] | GeoSurface | GeoVolume:
1461
1552
  """Compiles the striplines and returns a list of polygons or asingle one.
1462
1553
 
1463
1554
  The Z=0 argument determines the height of the striplines. Z=0 corresponds to the top of
@@ -160,11 +160,14 @@ class Sphere(GeoVolume):
160
160
  x,y,z = position
161
161
  self.tags: list[int] = [gmsh.model.occ.addSphere(x,y,z,radius),]
162
162
 
163
+ gmsh.model.occ.synchronize()
164
+ self._add_face_pointer('outside', tag=self.boundary().tags[0])
165
+
163
166
  @property
164
167
  def outside(self) -> FaceSelection:
165
168
  """The outside boundary of the sphere.
166
169
  """
167
- return self.boundary()
170
+ return self.face('outside')
168
171
 
169
172
  class XYPlate(GeoSurface):
170
173
  """Generates and XY-plane oriented plate
@@ -311,6 +314,7 @@ class Cylinder(GeoVolume):
311
314
  height*ax[0], height*ax[1], height*ax[2],
312
315
  radius)
313
316
  super().__init__(cyl, name=name)
317
+
314
318
  self._add_face_pointer('front', cs.origin, -cs.zax.np)
315
319
  self._add_face_pointer('back', cs.origin+height*cs.zax.np, cs.zax.np)
316
320
  self._add_face_pointer('bottom', cs.origin, -cs.zax.np)
@@ -471,15 +475,16 @@ class HalfSphere(GeoVolume):
471
475
  self._add_face_pointer('bottom',np.array(position), np.array(direction))
472
476
  self._add_face_pointer('face',np.array(position), np.array(direction))
473
477
  self._add_face_pointer('disc',np.array(position), np.array(direction))
474
-
478
+
479
+ gmsh.model.occ.synchronize()
480
+ self._add_face_pointer('outside', tag=self.boundary(exclude='disc').tags[0])
481
+
475
482
  @property
476
483
  def outside(self) -> FaceSelection:
477
- """The outside of the sphere excluding the flat disc face
478
-
479
- Returns:
480
- FaceSelection: _description_
484
+ """The outside boundary of the half sphere.
481
485
  """
482
- return self.boundary(exclude=('disc',))
486
+ return self.face('outside')
487
+
483
488
 
484
489
  @property
485
490
  def disc(self) -> FaceSelection: