emerge 0.5.1__py3-none-any.whl → 0.5.3__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 (54) hide show
  1. emerge/_emerge/bc.py +14 -20
  2. emerge/_emerge/const.py +5 -0
  3. emerge/_emerge/cs.py +2 -2
  4. emerge/_emerge/elements/femdata.py +14 -14
  5. emerge/_emerge/elements/index_interp.py +1 -1
  6. emerge/_emerge/elements/ned2_interp.py +1 -1
  7. emerge/_emerge/elements/nedelec2.py +4 -4
  8. emerge/_emerge/elements/nedleg2.py +10 -10
  9. emerge/_emerge/geo/horn.py +1 -1
  10. emerge/_emerge/geo/modeler.py +18 -19
  11. emerge/_emerge/geo/operations.py +13 -10
  12. emerge/_emerge/geo/pcb.py +180 -82
  13. emerge/_emerge/geo/pcb_tools/calculator.py +2 -2
  14. emerge/_emerge/geo/pcb_tools/macro.py +14 -13
  15. emerge/_emerge/geo/pmlbox.py +1 -1
  16. emerge/_emerge/geometry.py +47 -33
  17. emerge/_emerge/logsettings.py +15 -16
  18. emerge/_emerge/material.py +15 -11
  19. emerge/_emerge/mesh3d.py +81 -59
  20. emerge/_emerge/mesher.py +26 -21
  21. emerge/_emerge/mth/integrals.py +1 -1
  22. emerge/_emerge/mth/pairing.py +2 -2
  23. emerge/_emerge/periodic.py +34 -31
  24. emerge/_emerge/physics/microwave/adaptive_freq.py +15 -16
  25. emerge/_emerge/physics/microwave/assembly/assembler.py +120 -93
  26. emerge/_emerge/physics/microwave/assembly/curlcurl.py +1 -8
  27. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +43 -8
  28. emerge/_emerge/physics/microwave/assembly/robinbc.py +5 -5
  29. emerge/_emerge/physics/microwave/microwave_3d.py +71 -44
  30. emerge/_emerge/physics/microwave/microwave_bc.py +206 -117
  31. emerge/_emerge/physics/microwave/microwave_data.py +36 -38
  32. emerge/_emerge/physics/microwave/sc.py +26 -26
  33. emerge/_emerge/physics/microwave/simjob.py +20 -15
  34. emerge/_emerge/physics/microwave/sparam.py +12 -12
  35. emerge/_emerge/physics/microwave/touchstone.py +1 -1
  36. emerge/_emerge/plot/display.py +12 -6
  37. emerge/_emerge/plot/pyvista/display.py +44 -39
  38. emerge/_emerge/plot/pyvista/display_settings.py +1 -1
  39. emerge/_emerge/plot/simple_plots.py +15 -15
  40. emerge/_emerge/selection.py +35 -39
  41. emerge/_emerge/simmodel.py +41 -47
  42. emerge/_emerge/simulation_data.py +24 -15
  43. emerge/_emerge/solve_interfaces/cudss_interface.py +238 -0
  44. emerge/_emerge/solve_interfaces/pardiso_interface.py +24 -18
  45. emerge/_emerge/solver.py +314 -136
  46. emerge/cli.py +1 -1
  47. emerge/lib.py +245 -248
  48. {emerge-0.5.1.dist-info → emerge-0.5.3.dist-info}/METADATA +5 -1
  49. emerge-0.5.3.dist-info/RECORD +83 -0
  50. emerge/_emerge/plot/grapher.py +0 -93
  51. emerge-0.5.1.dist-info/RECORD +0 -82
  52. {emerge-0.5.1.dist-info → emerge-0.5.3.dist-info}/WHEEL +0 -0
  53. {emerge-0.5.1.dist-info → emerge-0.5.3.dist-info}/entry_points.txt +0 -0
  54. {emerge-0.5.1.dist-info → emerge-0.5.3.dist-info}/licenses/LICENSE +0 -0
@@ -16,13 +16,15 @@
16
16
  # <https://www.gnu.org/licenses/>.
17
17
 
18
18
  from __future__ import annotations
19
- import gmsh
19
+ import gmsh # type: ignore
20
20
  from .material import Material, AIR
21
- from .selection import FaceSelection, DomainSelection, EdgeSelection, PointSelection
21
+ from .selection import FaceSelection, DomainSelection, EdgeSelection, PointSelection, Selection
22
22
  from loguru import logger
23
- from typing import Literal, Any
23
+ from typing import Literal, Any, Iterable, TypeVar
24
24
  import numpy as np
25
25
 
26
+
27
+
26
28
  def _map_tags(tags: list[int], mapping: dict[int, list[int]]):
27
29
  new_tags = []
28
30
  for tag in tags:
@@ -48,12 +50,12 @@ class _GeometryManager:
48
50
  self.geometry_list: dict[str, list[GeoObject]] = dict()
49
51
  self.active: str = ''
50
52
 
51
- def all_geometries(self, model: str = None) -> list[GeoObject]:
53
+ def all_geometries(self, model: str | None = None) -> list[GeoObject]:
52
54
  if model is None:
53
55
  model = self.active
54
56
  return [geo for geo in self.geometry_list[model] if geo._exists]
55
57
 
56
- def submit_geometry(self, geo: GeoObject, model: str = None) -> None:
58
+ def submit_geometry(self, geo: GeoObject, model: str | None = None) -> None:
57
59
  if model is None:
58
60
  model = self.active
59
61
  self.geometry_list[model].append(geo)
@@ -135,7 +137,7 @@ class _FacePointer:
135
137
  def translate(self, dx, dy, dz):
136
138
  self.o = self.o + np.array([dx, dy, dz])
137
139
 
138
- def mirror(self, c0: np.ndarray, pln: np.ndarray):
140
+ def mirror(self, c0: np.ndarray, pln: np.ndarray) -> None:
139
141
  """
140
142
  Reflect self.o and self.n across the plane passing through c0
141
143
  with normal pln.
@@ -205,9 +207,11 @@ class GeoObject:
205
207
  """A generalization of any OpenCASCADE entity described by a dimension and a set of tags.
206
208
  """
207
209
  dim: int = -1
208
- def __init__(self):
210
+ def __init__(self, tags: list[int] | None = None):
211
+ if tags is None:
212
+ tags = []
209
213
  self.old_tags: list[int] = []
210
- self.tags: list[int] = []
214
+ self.tags: list[int] = tags
211
215
  self.material: Material = AIR
212
216
  self.mesh_multiplier: float = 1.0
213
217
  self.max_meshsize: float = 1e9
@@ -225,7 +229,7 @@ class GeoObject:
225
229
  _GEOMANAGER.submit_geometry(self)
226
230
 
227
231
  @property
228
- def color_rgb(self) -> tuple[int,int,int]:
232
+ def color_rgb(self) -> tuple[float, float, float]:
229
233
  return self.material.color_rgb
230
234
 
231
235
  @property
@@ -233,7 +237,7 @@ class GeoObject:
233
237
  return self.material.opacity
234
238
 
235
239
  @property
236
- def select(self) -> FaceSelection | DomainSelection | EdgeSelection | None:
240
+ def select(self) -> Selection:
237
241
  '''Returns a corresponding Face/Domain or Edge Selection object'''
238
242
  if self.dim==1:
239
243
  return EdgeSelection(self.tags)
@@ -241,11 +245,14 @@ class GeoObject:
241
245
  return FaceSelection(self.tags)
242
246
  elif self.dim==3:
243
247
  return DomainSelection(self.tags)
248
+ else:
249
+ return Selection(self.tags)
244
250
 
245
251
  @staticmethod
246
- def merged(objects: list[GeoObject]) -> list[GeoObject]:
252
+ def merged(objects: list[GeoPoint | GeoEdge | GeoSurface | GeoVolume | GeoObject]) -> list[GeoPoint | GeoEdge | GeoSurface | GeoVolume | GeoObject] | GeoPoint | GeoEdge | GeoSurface | GeoVolume | GeoObject:
247
253
  dim = objects[0].dim
248
254
  tags = []
255
+ out: GeoObject | None = None
249
256
  for obj in objects:
250
257
  tags.extend(obj.tags)
251
258
  if dim==2:
@@ -260,8 +267,8 @@ class GeoObject:
260
267
  def __repr__(self) -> str:
261
268
  return f'{self.__class__.__name__}({self.dim},{self.tags})'
262
269
 
263
- def _data(self, *labels) -> tuple[Any]:
264
- return tuple([self._aux_data.get(lab, None) for lab in labels])
270
+ def _data(self, *labels) -> tuple[Any | None, ...]:
271
+ return tuple([self._aux_data[lab] for lab in labels])
265
272
 
266
273
  def _add_face_pointer(self,
267
274
  name: str,
@@ -292,7 +299,7 @@ class GeoObject:
292
299
  for tag in self.tags:
293
300
  newtags.extend(tagmap.get(tag, [tag,]))
294
301
  self.tags = newtags
295
- logger.debug(f'Replaced {self.old_tags} with {self.tags}')
302
+ logger.debug(f'{self} Replaced {self.old_tags} -> {self.tags}')
296
303
 
297
304
  def update_tags(self, tag_mapping: dict[int,dict]) -> GeoObject:
298
305
  ''' Update the tag definition of a GeoObject after fragementation.'''
@@ -325,7 +332,7 @@ class GeoObject:
325
332
  self._tools.update(obj._tools)
326
333
  return self
327
334
 
328
- def _face_tags(self, name: FaceNames, tool: GeoObject = None) -> list[int]:
335
+ def _face_tags(self, name: FaceNames, tool: GeoObject | None = None) -> list[int]:
329
336
  names = self._all_pointer_names
330
337
  if name not in names:
331
338
  raise ValueError(f'The face {name} does not exist in {self}')
@@ -380,7 +387,7 @@ class GeoObject:
380
387
  self._priority -= 1
381
388
  return self
382
389
 
383
- def outside(self, *exclude: FaceNames, tags: list[int] = None) -> FaceSelection:
390
+ def outside(self, *exclude: FaceNames, tags: list[int] | None = None) -> FaceSelection:
384
391
  """Returns the complete set of outside faces.
385
392
 
386
393
  If implemented, it is possible to exclude a set of faces based on their name
@@ -394,7 +401,7 @@ class GeoObject:
394
401
  dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
395
402
  return FaceSelection([t for d,t in dimtags if t not in tags])
396
403
 
397
- def face(self, name: FaceNames, tool: GeoObject = None) -> FaceSelection:
404
+ def face(self, name: FaceNames, tool: GeoObject | None = None) -> FaceSelection:
398
405
  """Returns the FaceSelection for a given face name.
399
406
 
400
407
  The face name must be defined for the type of geometry.
@@ -422,7 +429,7 @@ class GeoObject:
422
429
  return FaceSelection([t[1] for t in tags])
423
430
  if self.dim == 2:
424
431
  return FaceSelection(self.tags)
425
- if self.dim < 2:
432
+ else:
426
433
  raise ValueError('Can only generate faces for objects of dimension 2 or higher.')
427
434
 
428
435
  @staticmethod
@@ -443,12 +450,13 @@ class GeoVolume(GeoObject):
443
450
  '''GeoVolume is an interface to the GMSH CAD kernel. It does not represent EMerge
444
451
  specific geometry data.'''
445
452
  dim = 3
446
- def __init__(self, tag: int | list[int]):
453
+ def __init__(self, tag: int | Iterable[int]):
447
454
  super().__init__()
448
- if isinstance(tag, list):
449
- self.tags: list[int] = tag
455
+ self.tags: list[int] = []
456
+ if isinstance(tag, Iterable):
457
+ self.tags = list(tag)
450
458
  else:
451
- self.tags: list[int] = [tag,]
459
+ self.tags = [tag,]
452
460
 
453
461
  @property
454
462
  def select(self) -> DomainSelection:
@@ -463,10 +471,12 @@ class GeoPoint(GeoObject):
463
471
 
464
472
  def __init__(self, tag: int | list[int]):
465
473
  super().__init__()
466
- if isinstance(tag, list):
467
- self.tags: list[int] = tag
474
+
475
+ self.tags: list[int] = []
476
+ if isinstance(tag, Iterable):
477
+ self.tags = list(tag)
468
478
  else:
469
- self.tags: list[int] = [tag,]
479
+ self.tags = [tag,]
470
480
 
471
481
  class GeoEdge(GeoObject):
472
482
  dim = 1
@@ -477,10 +487,11 @@ class GeoEdge(GeoObject):
477
487
 
478
488
  def __init__(self, tag: int | list[int]):
479
489
  super().__init__()
480
- if isinstance(tag, list):
481
- self.tags: list[int] = tag
490
+ self.tags: list[int] = []
491
+ if isinstance(tag, Iterable):
492
+ self.tags = list(tag)
482
493
  else:
483
- self.tags: list[int] = [tag,]
494
+ self.tags = [tag,]
484
495
 
485
496
 
486
497
  class GeoSurface(GeoObject):
@@ -494,17 +505,20 @@ class GeoSurface(GeoObject):
494
505
 
495
506
  def __init__(self, tag: int | list[int]):
496
507
  super().__init__()
497
- if isinstance(tag, list):
498
- self.tags: list[int] = tag
508
+ self.tags: list[int] = []
509
+ if isinstance(tag, Iterable):
510
+ self.tags = list(tag)
499
511
  else:
500
- self.tags: list[int] = [tag,]
512
+ self.tags = [tag,]
501
513
 
502
514
  class GeoPolygon(GeoSurface):
503
515
 
504
516
  def __init__(self,
505
517
  tags: list[int]):
506
518
  super().__init__(tags)
507
- self.points: list[int] = None
508
- self.lines: list[int] = None
519
+ self.points: list[int] = []
520
+ self.lines: list[int] = []
509
521
 
510
522
 
523
+ T = TypeVar('T', GeoVolume, GeoEdge, GeoPoint, GeoSurface)
524
+
@@ -4,31 +4,32 @@ from typing import Literal
4
4
  from enum import Enum
5
5
  from pathlib import Path
6
6
  import os
7
-
7
+ import gmsh
8
8
 
9
9
  ############################################################
10
10
  # FORMATS #
11
11
  ############################################################
12
12
 
13
13
  TRACE_FORMAT = (
14
- "{time: YY/MM/DD - (ddd) - HH:mm:ss.SSSS} | <green>{elapsed}</green> [ <level>{level}</level> ] "
15
- " <level>{message}</level>"
14
+ "{time:ddd YY/MM/DD HH:mm:ss.SSSS} {level:<7} {thread.id:<15} {line:>4}: "
15
+ "{message}"
16
16
  )
17
+
17
18
  DEBUG_FORMAT = (
18
- "<green>{elapsed}</green> [<level>{level}</level>] "
19
- " <level>{message}</level>"
19
+ "<green>{elapsed}</green> <level>{level:<7}</level>: "
20
+ "<level>{message}</level>"
20
21
  )
21
22
  INFO_FORMAT = (
22
- "<green>{elapsed}</green> [<level>{level}</level>] "
23
- " <level>{message}</level>"
23
+ "<green>{elapsed}</green> <level>{level:<7}</level>: "
24
+ "<level>{message}</level>"
24
25
  )
25
26
  WARNING_FORMAT = (
26
- "<green>{elapsed}</green> [<level>{level}</level>] "
27
- " <level>{message}</level>"
27
+ "<green>{elapsed}</green> <level>{level:<7}</level>: "
28
+ "<level>{message}</level>"
28
29
  )
29
30
  ERROR_FORMAT = (
30
- "<green>{elapsed}</green> [<level>{level}</level>] "
31
- " <level>{message}</level>"
31
+ "<green>{elapsed}</green> <level>{level:<7}</level>: "
32
+ "<level>{message}</level>"
32
33
  )
33
34
  FORMAT_DICT = {
34
35
  'TRACE': TRACE_FORMAT,
@@ -51,8 +52,8 @@ class LogController:
51
52
  logger.remove()
52
53
  self.std_handlers: list[int] = []
53
54
  self.file_handlers: list[int] = []
54
- self.level: LLTYPE = 'INFO'
55
- self.file_level: LLTYPE = 'INFO'
55
+ self.level: str = 'INFO'
56
+ self.file_level: str = 'INFO'
56
57
 
57
58
  def set_default(self):
58
59
  value = os.getenv("EMERGE_STD_LOGLEVEL", default="INFO")
@@ -68,13 +69,11 @@ class LogController:
68
69
  handler = {"sink": sys.stdout,
69
70
  "level": loglevel,
70
71
  "format": FORMAT_DICT.get(loglevel, INFO_FORMAT)}
71
- logger.configure(handlers=[handler])
72
+ logger.configure(handlers=[handler]) # type: ignore
72
73
  self.level = loglevel
73
74
  os.environ["EMERGE_STD_LOGLEVEL"] = loglevel
74
-
75
75
 
76
76
  def set_write_file(self, path: Path, loglevel: str = 'TRACE'):
77
-
78
77
  handler_id = logger.add(str(path / 'logging.log'), mode='w', level=loglevel, format=FORMAT_DICT.get(loglevel, INFO_FORMAT), colorize=False, backtrace=True, diagnose=True)
79
78
  self.file_handlers.append(handler_id)
80
79
  self.file_level = loglevel
@@ -17,7 +17,7 @@
17
17
 
18
18
  import numpy as np
19
19
  from dataclasses import dataclass
20
-
20
+ from typing import Callable
21
21
  @dataclass
22
22
  class Material:
23
23
  """The Material class generalizes a material in the EMerge FEM environment.
@@ -34,17 +34,21 @@ class Material:
34
34
  ur: float = 1
35
35
  tand: float = 0
36
36
  cond: float = 0
37
- _neff: float = None
38
- _fer: callable = None
39
- _fur: callable = None
37
+ _neff: float | None = None
38
+ _fer: Callable | None= None
39
+ _fur: Callable | None = None
40
40
  color: str = "#BEBEBE"
41
- _color_rgb: tuple[int,int,int] = None
41
+ _color_rgb: tuple[float,float,float] = (0.5, 0.5, 0.5)
42
42
  opacity: float = 1.0
43
43
 
44
44
  def __post_init__(self):
45
45
  hex_str = self.color.lstrip('#')
46
46
  self._color_rgb = tuple(int(hex_str[i:i+2], 16)/255.0 for i in (0, 2, 4))
47
47
 
48
+ @property
49
+ def sigma(self) -> float:
50
+ return self.cond
51
+
48
52
  @property
49
53
  def color_rgb(self) -> tuple[float,float,float]:
50
54
  return self._color_rgb
@@ -73,34 +77,34 @@ class Material:
73
77
  return np.abs(np.sqrt(er*(1-1j*self.tand)*ur))
74
78
 
75
79
  @property
76
- def fer2d(self) -> callable:
80
+ def fer2d(self) -> Callable:
77
81
  if self._fer is None:
78
82
  return lambda x,y: self.er*(1-1j*self.tand)*np.ones_like(x)
79
83
  else:
80
84
  return self._fer
81
85
 
82
86
  @property
83
- def fur2d(self) -> callable:
87
+ def fur2d(self) -> Callable:
84
88
  if self._fur is None:
85
89
 
86
90
  return lambda x,y: self.ur*np.ones_like(x)
87
91
  else:
88
92
  return self._fur
89
93
  @property
90
- def fer3d(self) -> callable:
94
+ def fer3d(self) -> Callable:
91
95
  if self._fer is None:
92
96
  return lambda x,y,z: self.er*(1-1j*self.tand)*np.ones_like(x)
93
97
  else:
94
98
  return self._fer
95
99
 
96
100
  @property
97
- def fur3d(self) -> callable:
101
+ def fur3d(self) -> Callable:
98
102
  if self._fur is None:
99
103
  return lambda x,y,z: self.ur*np.ones_like(x)
100
104
  else:
101
105
  return self._fur
102
106
  @property
103
- def fer3d_mat(self) -> callable:
107
+ def fer3d_mat(self) -> Callable:
104
108
  if self._fer is None:
105
109
 
106
110
  return lambda x,y,z: np.repeat(self.ermat[:, :, np.newaxis], x.shape[0], axis=2)
@@ -108,7 +112,7 @@ class Material:
108
112
  return self._fer
109
113
 
110
114
  @property
111
- def fur3d_mat(self) -> callable:
115
+ def fur3d_mat(self) -> Callable:
112
116
  if self._fur is None:
113
117
  return lambda x,y,z: np.repeat(self.urmat[:, :, np.newaxis], x.shape[0], axis=2)
114
118
  else:
emerge/_emerge/mesh3d.py CHANGED
@@ -16,11 +16,11 @@
16
16
  # <https://www.gnu.org/licenses/>.
17
17
 
18
18
  from __future__ import annotations
19
- import gmsh
19
+ import gmsh # type: ignore
20
20
  import numpy as np
21
- from numba import njit, f8
21
+ from numba import njit, f8 # type: ignore
22
22
  from .mesher import Mesher
23
- from typing import Union, List, Tuple, Callable
23
+ from typing import Union, List, Tuple, Callable, Any
24
24
  from collections import defaultdict
25
25
  from .geometry import GeoVolume
26
26
  from .mth.optimized import outward_normal
@@ -75,8 +75,10 @@ def tri_ordering(i1: int, i2: int, i3: int) -> int:
75
75
  '''
76
76
  return np.sign(np.sign(i2-1) + np.sign(i3-i2) + np.sign(i1-i3))
77
77
 
78
+ class Mesh:
79
+ pass
78
80
 
79
- class Mesh3D:
81
+ class Mesh3D(Mesh):
80
82
  """A Mesh managing all 3D mesh related properties.
81
83
 
82
84
  Relevant mesh data such as mappings between nodes(vertices), edges, triangles and tetrahedra
@@ -89,54 +91,54 @@ class Mesh3D:
89
91
  self.geometry: Mesher = mesher
90
92
 
91
93
  # All spatial objects
92
- self.nodes: np.ndarray = None
93
- self.n_i2t: dict = None
94
- self.n_t2i: dict = None
94
+ self.nodes: np.ndarray = np.array([])
95
+ self.n_i2t: dict = dict()
96
+ self.n_t2i: dict = dict()
95
97
 
96
98
  # tets colletions
97
- self.tets: np.ndarray = None
98
- self.tet_i2t: dict = None
99
- self.tet_t2i: dict = None
100
- self.centers: np.ndarray = None
99
+ self.tets: np.ndarray = np.array([])
100
+ self.tet_i2t: dict = dict()
101
+ self.tet_t2i: dict = dict()
102
+ self.centers: np.ndarray = np.array([])
101
103
 
102
104
  # triangles
103
- self.tris: np.ndarray = None
104
- self.tri_i2t: dict = None
105
- self.tri_t2i: dict = None
106
- self.areas: np.ndarray = None
107
- self.tri_centers: np.ndarray = None
105
+ self.tris: np.ndarray = np.array([])
106
+ self.tri_i2t: dict = dict()
107
+ self.tri_t2i: dict = dict()
108
+ self.areas: np.ndarray = np.array([])
109
+ self.tri_centers: np.ndarray = np.array([])
108
110
 
109
111
  # edges
110
- self.edges: np.ndarray = None
111
- self.edge_i2t: dict = None
112
- self.edge_t2i: dict = None
113
- self.edge_centers: np.ndarray = None
114
- self.edge_lengths: np.ndarray = None
112
+ self.edges: np.ndarray = np.array([])
113
+ self.edge_i2t: dict = dict()
114
+ self.edge_t2i: dict = dict()
115
+ self.edge_centers: np.ndarray = np.array([])
116
+ self.edge_lengths: np.ndarray = np.array([])
115
117
 
116
118
  # Inverse mappings
117
- self.inv_edges: dict = None
118
- self.inv_tris: dict = None
119
- self.inv_tets: dict = None
119
+ self.inv_edges: dict = dict()
120
+ self.inv_tris: dict = dict()
121
+ self.inv_tets: dict = dict()
120
122
 
121
123
  # Mappings
122
124
 
123
- self.tet_to_edge: np.ndarray = None
124
- self.tet_to_edge_sign: np.ndarray = None
125
- self.tet_to_tri: np.ndarray = None
126
- self.tri_to_tet: np.ndarray = None
127
- self.tri_to_edge: np.ndarray = None
128
- self.tri_to_edge_sign: np.ndarray = None
129
- self.edge_to_tri: defaultdict = None
130
- self.node_to_edge: defaultdict = None
125
+ self.tet_to_edge: np.ndarray = np.array([])
126
+ self.tet_to_edge_sign: np.ndarray = np.array([])
127
+ self.tet_to_tri: np.ndarray = np.array([])
128
+ self.tri_to_tet: np.ndarray = np.array([])
129
+ self.tri_to_edge: np.ndarray = np.array([])
130
+ self.tri_to_edge_sign: np.ndarray = np.array([])
131
+ self.edge_to_tri: defaultdict | dict = defaultdict()
132
+ self.node_to_edge: defaultdict | dict = defaultdict()
131
133
 
132
134
  # Physics mappings
133
135
 
134
- self.tet_to_field: np.ndarray = None
135
- self.edge_to_field: np.ndarray = None
136
- self.tri_to_field: np.ndarray = None
136
+ self.tet_to_field: np.ndarray = np.array([])
137
+ self.edge_to_field: np.ndarray = np.array([])
138
+ self.tri_to_field: np.ndarray = np.array([])
137
139
 
138
140
  ## States
139
- self.defined = False
141
+ self.defined: bool = False
140
142
 
141
143
 
142
144
  ## Memory
@@ -144,6 +146,8 @@ class Mesh3D:
144
146
  self.ftag_to_node: dict[int, list[int]] = dict()
145
147
  self.ftag_to_edge: dict[int, list[int]] = dict()
146
148
  self.vtag_to_tet: dict[int, list[int]] = dict()
149
+
150
+ self.exterior_face_tags: list[int] = []
147
151
 
148
152
  @property
149
153
  def n_edges(self) -> int:
@@ -170,7 +174,9 @@ class Mesh3D:
170
174
  if i1==i2:
171
175
  raise ValueError("Edge cannot be formed by the same node.")
172
176
  search = (min(int(i1),int(i2)), max(int(i1),int(i2)))
173
- result = self.inv_edges.get(search, None)
177
+ result = self.inv_edges.get(search, -10)
178
+ if result == -10:
179
+ ValueError(f'There is no edge with indices {i1}, {i2}')
174
180
  return result
175
181
 
176
182
  def get_edge_sign(self, i1: int, i2: int) -> int:
@@ -183,13 +189,19 @@ class Mesh3D:
183
189
 
184
190
  def get_tri(self, i1, i2, i3) -> int:
185
191
  '''Return the triangle index given the three node indices'''
186
- return self.inv_tris.get(tuple(sorted((int(i1), int(i2), int(i3)))), None)
192
+ output = self.inv_tris.get(tuple(sorted((int(i1), int(i2), int(i3)))), None)
193
+ if output is None:
194
+ raise ValueError(f'There is no triangle with indices {i1}, {i2}, {i3}')
195
+ return output
187
196
 
188
197
  def get_tet(self, i1, i2, i3, i4) -> int:
189
198
  '''Return the tetrahedron index given the four node indices'''
190
- return self.inv_tets.get(tuple(sorted((int(i1), int(i2), int(i3), int(i4)))), None)
199
+ output = self.inv_tets.get(tuple(sorted((int(i1), int(i2), int(i3), int(i4)))), None)
200
+ if output is None:
201
+ raise ValueError(f'There is no tetrahedron with indices {i1}, {i2}, {i3}, {i4}')
202
+ return output
191
203
 
192
- def boundary_triangles(self, dimtags: list[tuple[int, int]] = None) -> np.ndarray:
204
+ def boundary_triangles(self, dimtags: list[tuple[int, int]] | None = None) -> np.ndarray:
193
205
  if dimtags is None:
194
206
  outputtags = []
195
207
  for tags in self.ftag_to_tri.values():
@@ -230,7 +242,7 @@ class Mesh3D:
230
242
 
231
243
  def get_face_tets(self, *taglist: list[int]) -> np.ndarray:
232
244
  ''' Return a list of a tetrahedrons that share a node with any of the nodes in the provided face.'''
233
- nodes = set()
245
+ nodes: set = set()
234
246
  for tags in taglist:
235
247
  nodes.update(self.get_nodes(tags))
236
248
  return np.array([i for i, tet in enumerate(self.tets.T) if not set(tet).isdisjoint(nodes)])
@@ -258,7 +270,7 @@ class Mesh3D:
258
270
  return np.array(sorted(list(set(edges))))
259
271
 
260
272
 
261
- def update(self, periodic_bcs: list[Periodic] = None):
273
+ def update(self, periodic_bcs: list[Periodic] | None = None):
262
274
  if periodic_bcs is None:
263
275
  periodic_bcs = []
264
276
 
@@ -406,8 +418,7 @@ class Mesh3D:
406
418
  self.edge_centers = (self.nodes[:,self.edges[0,:]] + self.nodes[:,self.edges[1,:]]) / 2
407
419
  self.edge_lengths = np.sqrt(np.sum((self.nodes[:,self.edges[0,:]] - self.nodes[:,self.edges[1,:]])**2, axis=0))
408
420
  self.areas = np.array([area(self.nodes[:,self.tris[0,i]], self.nodes[:,self.tris[1,i]], self.nodes[:,self.tris[2,i]]) for i in range(self.tris.shape[1])])
409
-
410
-
421
+
411
422
  ## Tag bindings
412
423
  face_dimtags = gmsh.model.get_entities(2)
413
424
  for d,t in face_dimtags:
@@ -466,11 +477,11 @@ class Mesh3D:
466
477
  all_node_ids = np.unique(np.array(node_ids_1 + node_ids_2))
467
478
  dsmin = shortest_distance(self.nodes[:,all_node_ids])
468
479
 
469
- node_ids_1 = sorted(list(set(node_ids_1)))
470
- node_ids_2 = sorted(list(set(node_ids_2)))
480
+ node_ids_1_arry = np.sort(np.unique(np.array(node_ids_1)))
481
+ node_ids_2_arry = np.sort(np.unique(np.array(node_ids_2)))
471
482
  dv = np.array(bc.dv)
472
483
 
473
- nodemap = pair_coordinates(self.nodes, node_ids_1, node_ids_2, dv, dsmin/2)
484
+ nodemap = pair_coordinates(self.nodes, node_ids_1_arry, node_ids_2_arry, dv, dsmin/2)
474
485
  node_ids_2_unsorted = [nodemap[i] for i in sorted(node_ids_1)]
475
486
  node_ids_2_sorted = sorted(node_ids_2_unsorted)
476
487
  conv_map = {i1: i2 for i1, i2 in zip(node_ids_2_unsorted, node_ids_2_sorted)}
@@ -499,7 +510,7 @@ class Mesh3D:
499
510
  def plot_gmsh(self) -> None:
500
511
  gmsh.fltk.run()
501
512
 
502
- def find_edge_groups(self, edge_ids: np.ndarray) -> dict:
513
+ def find_edge_groups(self, edge_ids: np.ndarray) -> list[tuple[Any,...]]:
503
514
  """
504
515
  Find the groups of edges in the mesh.
505
516
 
@@ -568,7 +579,7 @@ class Mesh3D:
568
579
 
569
580
  def boundary_surface(self,
570
581
  face_tags: Union[int, list[int]],
571
- origin: tuple[float, float, float] = None) -> SurfaceMesh:
582
+ origin: tuple[float, float, float] | None = None) -> SurfaceMesh:
572
583
  """Returns a SurfaceMesh class that is a 2D mesh isolated from the 3D mesh
573
584
 
574
585
  The mesh will be based on the given set of face tags.
@@ -593,7 +604,7 @@ class Mesh3D:
593
604
 
594
605
  return SurfaceMesh(self, tri_ids, origin)
595
606
 
596
- class SurfaceMesh:
607
+ class SurfaceMesh(Mesh):
597
608
 
598
609
  def __init__(self,
599
610
  original: Mesh3D,
@@ -614,21 +625,22 @@ class SurfaceMesh:
614
625
 
615
626
  self.original_tris: np.ndarray = original.tris
616
627
 
617
- self.old_new_node_map: dict[int,int] = old_to_new_node_id_map
628
+ self.old_new_node_map: dict = old_to_new_node_id_map
618
629
  self.original: Mesh3D = original
619
630
  self._alignment_origin: np.ndarray = np.array(origin).astype(np.float64)
620
631
  self.nodes: np.ndarray = original.nodes[:, unique_nodes]
621
632
  self.tris: np.ndarray = new_tris
622
633
 
623
634
  ## initialize derived
624
- self.edge_centers: np.ndarray = None
625
- self.edge_tris: np.ndarray = None
635
+ self.edge_centers: np.ndarray = np.array([])
636
+ self.edge_tris: np.ndarray = np.array([])
626
637
  self.n_nodes = self.nodes.shape[1]
627
638
  self.n_tris = self.tris.shape[1]
628
- self.n_edges = None
629
- self.areas: np.ndarray = None
630
- self.normals: np.ndarray = None
631
-
639
+ self.n_edges: float = -1
640
+ self.areas: np.ndarray = np.array([])
641
+ self.normals: np.ndarray = np.array([])
642
+ self.tri_to_edge: np.ndarray = np.array([])
643
+ self.edge_to_tri: dict | defaultdict = dict()
632
644
  # Generate derived
633
645
  self.update()
634
646
 
@@ -642,22 +654,26 @@ class SurfaceMesh:
642
654
  self.flipY()
643
655
  if ax.lower()=='z':
644
656
  self.flipZ()
657
+ return self
645
658
  #self.tris[(0,1),:] = self.tris[(1,0),:]
646
659
 
647
660
  def flipX(self) -> SurfaceMesh:
648
661
  self.nodes[0,:] = -self.nodes[0,:]
649
662
  self.normals[0,:] = -self.normals[0,:]
650
663
  self.edge_centers[0,:] = -self.edge_centers[0,:]
651
-
664
+ return self
665
+
652
666
  def flipY(self) -> SurfaceMesh:
653
667
  self.nodes[1,:] = -self.nodes[1,:]
654
668
  self.normals[1,:] = -self.normals[1,:]
655
669
  self.edge_centers[1,:] = -self.edge_centers[1,:]
670
+ return self
656
671
 
657
672
  def flipZ(self) -> SurfaceMesh:
658
673
  self.nodes[2,:] = -self.nodes[2,:]
659
674
  self.normals[2,:] = -self.normals[2,:]
660
675
  self.edge_centers[2,:] = -self.edge_centers[2,:]
676
+ return self
661
677
 
662
678
  def from_source_tri(self, triid: int) -> int | None:
663
679
  ''' Returns a triangle index from the old mesh to the new mesh.'''
@@ -685,6 +701,8 @@ class SurfaceMesh:
685
701
  raise ValueError("Edge cannot be formed by the same node.")
686
702
  search = (min(int(i1),int(i2)), max(int(i1),int(i2)))
687
703
  result = self.inv_edges.get(search, None)
704
+ if result is None:
705
+ raise ValueError(f'There is no edge with indices {i1}, {i2}')
688
706
  return result
689
707
 
690
708
  def get_edge_sign(self, i1: int, i2: int) -> int:
@@ -697,7 +715,11 @@ class SurfaceMesh:
697
715
 
698
716
  def get_tri(self, i1, i2, i3) -> int:
699
717
  '''Return the triangle index given the three node indices'''
700
- return self.inv_tris.get(tuple(sorted((int(i1), int(i2), int(i3)))), None)
718
+ result = self.inv_tris.get(tuple(sorted((int(i1), int(i2), int(i3)))), None)
719
+ if result is None:
720
+ raise ValueError(f'There is no triangle with indices {i1}, {i2}, {i3}')
721
+ return result
722
+
701
723
 
702
724
  def update(self) -> None:
703
725
  ## First Edges