emerge 0.4.7__py3-none-any.whl → 0.4.9__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 (78) hide show
  1. emerge/__init__.py +14 -14
  2. emerge/_emerge/__init__.py +42 -0
  3. emerge/_emerge/bc.py +197 -0
  4. emerge/_emerge/coord.py +119 -0
  5. emerge/_emerge/cs.py +523 -0
  6. emerge/_emerge/dataset.py +36 -0
  7. emerge/_emerge/elements/__init__.py +19 -0
  8. emerge/_emerge/elements/femdata.py +212 -0
  9. emerge/_emerge/elements/index_interp.py +64 -0
  10. emerge/_emerge/elements/legrange2.py +172 -0
  11. emerge/_emerge/elements/ned2_interp.py +645 -0
  12. emerge/_emerge/elements/nedelec2.py +140 -0
  13. emerge/_emerge/elements/nedleg2.py +217 -0
  14. emerge/_emerge/geo/__init__.py +24 -0
  15. emerge/_emerge/geo/horn.py +107 -0
  16. emerge/_emerge/geo/modeler.py +449 -0
  17. emerge/_emerge/geo/operations.py +254 -0
  18. emerge/_emerge/geo/pcb.py +1244 -0
  19. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  20. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  21. emerge/_emerge/geo/pmlbox.py +204 -0
  22. emerge/_emerge/geo/polybased.py +529 -0
  23. emerge/_emerge/geo/shapes.py +427 -0
  24. emerge/_emerge/geo/step.py +77 -0
  25. emerge/_emerge/geo2d.py +86 -0
  26. emerge/_emerge/geometry.py +510 -0
  27. emerge/_emerge/howto.py +214 -0
  28. emerge/_emerge/logsettings.py +5 -0
  29. emerge/_emerge/material.py +118 -0
  30. emerge/_emerge/mesh3d.py +730 -0
  31. emerge/_emerge/mesher.py +339 -0
  32. emerge/_emerge/mth/common_functions.py +33 -0
  33. emerge/_emerge/mth/integrals.py +71 -0
  34. emerge/_emerge/mth/optimized.py +357 -0
  35. emerge/_emerge/periodic.py +263 -0
  36. emerge/_emerge/physics/__init__.py +0 -0
  37. emerge/_emerge/physics/microwave/__init__.py +1 -0
  38. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  39. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  40. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  41. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  42. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  43. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  44. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  45. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  46. emerge/_emerge/physics/microwave/periodic.py +82 -0
  47. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  48. emerge/_emerge/physics/microwave/sc.py +175 -0
  49. emerge/_emerge/physics/microwave/simjob.py +147 -0
  50. emerge/_emerge/physics/microwave/sparam.py +138 -0
  51. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  52. emerge/_emerge/plot/__init__.py +0 -0
  53. emerge/_emerge/plot/display.py +394 -0
  54. emerge/_emerge/plot/grapher.py +93 -0
  55. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  56. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  57. emerge/_emerge/plot/pyvista/display.py +931 -0
  58. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  59. emerge/_emerge/plot/simple_plots.py +551 -0
  60. emerge/_emerge/plot.py +225 -0
  61. emerge/_emerge/projects/__init__.py +0 -0
  62. emerge/_emerge/projects/_gen_base.txt +32 -0
  63. emerge/_emerge/projects/_load_base.txt +24 -0
  64. emerge/_emerge/projects/generate_project.py +40 -0
  65. emerge/_emerge/selection.py +596 -0
  66. emerge/_emerge/simmodel.py +444 -0
  67. emerge/_emerge/simulation_data.py +411 -0
  68. emerge/_emerge/solver.py +993 -0
  69. emerge/_emerge/system.py +54 -0
  70. emerge/cli.py +19 -0
  71. emerge/lib.py +1 -1
  72. emerge/plot.py +1 -1
  73. {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/METADATA +7 -6
  74. emerge-0.4.9.dist-info/RECORD +78 -0
  75. emerge-0.4.9.dist-info/entry_points.txt +2 -0
  76. emerge-0.4.7.dist-info/RECORD +0 -9
  77. emerge-0.4.7.dist-info/entry_points.txt +0 -2
  78. {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/WHEEL +0 -0
@@ -0,0 +1,730 @@
1
+ # EMerge is an open source Python based FEM EM simulation module.
2
+ # Copyright (C) 2025 Robert Fennis.
3
+
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 2
7
+ # of the License, or (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see
16
+ # <https://www.gnu.org/licenses/>.
17
+
18
+ from __future__ import annotations
19
+ import gmsh
20
+ import numpy as np
21
+ from numba import njit, f8
22
+ from .mesher import Mesher
23
+ from typing import Union, List, Tuple, Callable
24
+ from collections import defaultdict
25
+ from .geometry import GeoVolume
26
+ from .mth.optimized import outward_normal
27
+ from loguru import logger
28
+ from functools import cache
29
+ from .bc import Periodic
30
+
31
+ @njit(f8(f8[:], f8[:], f8[:]), cache=True, nogil=True)
32
+ def area(x1: np.ndarray, x2: np.ndarray, x3: np.ndarray):
33
+ e1 = x2 - x1
34
+ e2 = x3 - x1
35
+ av = np.array([e1[1]*e2[2] - e1[2]*e2[1], e1[2]*e2[0] - e1[0]*e2[2], e1[0]*e2[1] - e1[1]*e2[0]])
36
+ return np.sqrt(av[0]**2 + av[1]**2 + av[2]**2)/2
37
+
38
+ def shortest_distance(point_cloud):
39
+ """
40
+ Compute the shortest distance between any two points in a 3D point cloud.
41
+
42
+ Parameters:
43
+ - point_cloud: np.ndarray of shape (3, N)
44
+
45
+ Returns:
46
+ - min_dist: float, the shortest distance
47
+ """
48
+ # Transpose to shape (N, 3)
49
+ points = point_cloud.T # Shape (N, 3)
50
+
51
+ # Compute pairwise squared distances (broadcasting)
52
+ diff = points[:, np.newaxis, :] - points[np.newaxis, :, :] # Shape (N, N, 3)
53
+ dist_sq = np.einsum('ijk,ijk->ij', diff, diff) # Shape (N, N)
54
+
55
+ # Avoid zero on diagonal (distance to self), set to np.inf
56
+ np.fill_diagonal(dist_sq, np.inf)
57
+
58
+ # Return minimum distance
59
+ return np.sqrt(np.min(dist_sq))
60
+
61
+ def tri_ordering(i1: int, i2: int, i3: int) -> int:
62
+ ''' Takes two integer indices of triangle verticces and determines if they are in increasing order or decreasing order.
63
+ It ignores cyclic shifts of the indices. so (4,10,21) == (10,21,4) == (21,4,10)
64
+
65
+ for triangle (4,10,20) (for example)
66
+ (4,10,20): In co-ordering of phase = 0: i1 < i2, i2 < i3, i3 > i1: 4-10-20-4 diffs = +6 +10 -16
67
+ (10,20,4): In co-order shift 1: i1 < i2, i2 > i3, i3 < i1: 10-20-4-10 diffs = 10 -16 - (6)
68
+ (20,4,10): In co-order shift 2: i1 > i2, i2 < i3, i3 < i1:
69
+
70
+ For triangle (20,10,4)
71
+ (20,10,3): i1 > i2, i2 > i3
72
+ (10,3,20): i1 > i2, i2 < i3
73
+ (3,20,10): i1 < i2, i2 > i3
74
+ '''
75
+ return np.sign(np.sign(i2-1) + np.sign(i3-i2) + np.sign(i1-i3))
76
+
77
+
78
+ class Mesh3D:
79
+ """A Mesh managing all 3D mesh related properties.
80
+
81
+ Relevant mesh data such as mappings between nodes(vertices), edges, triangles and tetrahedra
82
+ are managed by the Mesh3D class. Specific information regarding to how actual field values
83
+ are mapped to mesh elements is managed by the FEMBasis class.
84
+
85
+ """
86
+ def __init__(self, mesher: Mesher):
87
+
88
+ self.geometry: Mesher = mesher
89
+
90
+ # All spatial objects
91
+ self.nodes: np.ndarray = None
92
+ self.n_i2t: dict = None
93
+ self.n_t2i: dict = None
94
+
95
+ # tets colletions
96
+ self.tets: np.ndarray = None
97
+ self.tet_i2t: dict = None
98
+ self.tet_t2i: dict = None
99
+ self.centers: np.ndarray = None
100
+
101
+ # triangles
102
+ self.tris: np.ndarray = None
103
+ self.tri_i2t: dict = None
104
+ self.tri_t2i: dict = None
105
+ self.areas: np.ndarray = None
106
+ self.tri_centers: np.ndarray = None
107
+
108
+ # edges
109
+ self.edges: np.ndarray = None
110
+ self.edge_i2t: dict = None
111
+ self.edge_t2i: dict = None
112
+ self.edge_centers: np.ndarray = None
113
+ self.edge_lengths: np.ndarray = None
114
+
115
+ # Inverse mappings
116
+ self.inv_edges: dict = None
117
+ self.inv_tris: dict = None
118
+ self.inv_tets: dict = None
119
+
120
+ # Mappings
121
+
122
+ self.tet_to_edge: np.ndarray = None
123
+ self.tet_to_edge_sign: np.ndarray = None
124
+ self.tet_to_tri: np.ndarray = None
125
+ self.tri_to_tet: np.ndarray = None
126
+ self.tri_to_edge: np.ndarray = None
127
+ self.tri_to_edge_sign: np.ndarray = None
128
+ self.edge_to_tri: defaultdict = None
129
+ self.node_to_edge: defaultdict = None
130
+
131
+ # Physics mappings
132
+
133
+ self.tet_to_field: np.ndarray = None
134
+ self.edge_to_field: np.ndarray = None
135
+ self.tri_to_field: np.ndarray = None
136
+
137
+ ## States
138
+ self.defined = False
139
+
140
+
141
+ ## Memory
142
+ self.ftag_to_tri: dict[int, list[int]] = dict()
143
+ self.ftag_to_node: dict[int, list[int]] = dict()
144
+ self.vtag_to_tet: dict[int, list[int]] = dict()
145
+
146
+ @property
147
+ def n_edges(self) -> int:
148
+ '''Return the number of edges'''
149
+ return self.edges.shape[1]
150
+
151
+ @property
152
+ def n_tets(self) -> int:
153
+ '''Return the number of tets'''
154
+ return self.tets.shape[1]
155
+
156
+ @property
157
+ def n_tris(self) -> int:
158
+ '''Return the number of triangles'''
159
+ return self.tris.shape[1]
160
+
161
+ @property
162
+ def n_nodes(self) -> int:
163
+ '''Return the number of nodes'''
164
+ return self.nodes.shape[1]
165
+
166
+ def get_edge(self, i1: int, i2: int) -> int:
167
+ '''Return the edge index given the two node indices'''
168
+ if i1==i2:
169
+ raise ValueError("Edge cannot be formed by the same node.")
170
+ search = (min(int(i1),int(i2)), max(int(i1),int(i2)))
171
+ result = self.inv_edges.get(search, None)
172
+ return result
173
+
174
+ def get_edge_sign(self, i1: int, i2: int) -> int:
175
+ '''Return the edge index given the two node indices'''
176
+ if i1==i2:
177
+ raise ValueError("Edge cannot be formed by the same node.")
178
+ if i1 > i2:
179
+ return -1
180
+ return 1
181
+
182
+ def get_tri(self, i1, i2, i3) -> int:
183
+ '''Return the triangle index given the three node indices'''
184
+ return self.inv_tris.get(tuple(sorted((int(i1), int(i2), int(i3)))), None)
185
+
186
+ def get_tet(self, i1, i2, i3, i4) -> int:
187
+ '''Return the tetrahedron index given the four node indices'''
188
+ return self.inv_tets.get(tuple(sorted((int(i1), int(i2), int(i3), int(i4)))), None)
189
+
190
+ def boundary_triangles(self, dimtags: list[tuple[int, int]] = None) -> np.ndarray:
191
+ if dimtags is None:
192
+ outputtags = []
193
+ for tags in self.ftag_to_tri.values():
194
+ outputtags.extend(tags)
195
+ return np.array(outputtags)
196
+ else:
197
+ dts = []
198
+ for dimtag in dimtags:
199
+ if dimtag[0]==2:
200
+ dts.append(dimtag)
201
+ elif dimtag[0]==3:
202
+ dts.extend(gmsh.model.get_boundary(dimtags))
203
+ return self.get_triangles([tag[1] for tag in dts])
204
+
205
+
206
+ def get_tetrahedra(self, vol_tags: Union[int, list[int]]) -> np.ndarray:
207
+ if isinstance(vol_tags, int):
208
+ vol_tags = [vol_tags,]
209
+
210
+ indices = []
211
+ for voltag in vol_tags:
212
+ indices.extend(self.vtag_to_tet[voltag])
213
+ return np.array(indices)
214
+
215
+ def get_triangles(self, face_tags: Union[int, list[int]]) -> np.ndarray:
216
+ '''Returns a numpyarray of all the triangles that belong to the given face tags'''
217
+ if isinstance(face_tags, int):
218
+ face_tags = [face_tags,]
219
+ indices = []
220
+ for facetag in face_tags:
221
+ indices.extend(self.ftag_to_tri[facetag])
222
+ if any([(i is None) for i in indices]):
223
+ logger.error('Clearing None indices: ', [i for i, ind in enumerate(indices) if ind is None])
224
+ logger.error('This is usually a sign of boundaries sticking out of domains. Please check your Geometry.')
225
+ indices = [i for i in indices if i is not None]
226
+
227
+ return np.array(indices)
228
+
229
+ def get_face_tets(self, *taglist: list[int]) -> np.ndarray:
230
+ ''' Return a list of a tetrahedrons that share a node with any of the nodes in the provided face.'''
231
+ nodes = set()
232
+ for tags in taglist:
233
+ nodes.update(self.get_nodes(tags))
234
+ return np.array([i for i, tet in enumerate(self.tets.T) if not set(tet).isdisjoint(nodes)])
235
+
236
+ def get_nodes(self, face_tags: Union[int, list[int]]) -> np.ndarray:
237
+ '''Returns a numpyarray of all the nodes that belong to the given face tags'''
238
+ if isinstance(face_tags, int):
239
+ face_tags = [face_tags,]
240
+
241
+ nodes = []
242
+ for facetag in face_tags:
243
+ nodes.extend(self.ftag_to_node[facetag])
244
+
245
+ return np.array(sorted(list(set(nodes))))
246
+
247
+
248
+ def update(self, periodic_bcs: list[Periodic] = None):
249
+ if periodic_bcs is None:
250
+ periodic_bcs = []
251
+
252
+ nodes, lin_coords, _ = gmsh.model.mesh.get_nodes()
253
+
254
+ coords = lin_coords.reshape(-1, 3).T
255
+
256
+ ## Vertices
257
+ self.nodes = coords
258
+ self.n_i2t = {i: int(t) for i, t in enumerate(nodes)}
259
+ self.n_t2i = {t: i for i, t in self.n_i2t.items()}
260
+
261
+ ## Tetrahedras
262
+
263
+ _, tet_tags, tet_node_tags = gmsh.model.mesh.get_elements(3)
264
+
265
+ # The algorithm assumes that only one domain tag is returned in this function.
266
+ # Hence the use of tri_node_tags[0] in the next line. If domains are missing.
267
+ # Make sure to combine all the entries in the tri-node-tags list
268
+
269
+ tet_node_tags = [self.n_t2i[int(t)] for t in tet_node_tags[0]]
270
+ tet_tags = np.squeeze(np.array(tet_tags))
271
+
272
+ self.tets = np.array(tet_node_tags).reshape(-1,4).T
273
+ self.tet_i2t = {i: int(t) for i, t in enumerate(tet_tags)}
274
+ self.tet_t2i = {t: i for i, t in self.tet_i2t.items()}
275
+
276
+ self.centers = (self.nodes[:,self.tets[0,:]] + self.nodes[:,self.tets[1,:]] + self.nodes[:,self.tets[2,:]] + self.nodes[:,self.tets[3,:]]) / 4
277
+
278
+ # Resort node indices to be sorted on all periodic conditions
279
+ # This sorting makes sure that each edge and triangle on a source face is
280
+ # sorted in the same order as the corresponding target face triangle or edge.
281
+ # In other words, if a source face triangle or edge index i1, i2, i3 is mapped to j1, j2, j3 respectively
282
+ # Then this ensures that if i1>i2>i3 then j1>j2>j3
283
+
284
+ for bc in periodic_bcs:
285
+ nodemap, ids1, ids2 = self._derive_node_map(bc)
286
+ nodemap = {int(a): int(b) for a,b in nodemap.items()}
287
+ self.nodes[:,ids2] = self.nodes[:,ids1]
288
+ for itet in range(self.tets.shape[1]):
289
+ self.tets[:,itet] = [nodemap.get(i, i) for i in self.tets[:,itet]]
290
+ self.n_t2i = {t: nodemap.get(i,i) for t,i in self.n_t2i.items()}
291
+ self.n_i2t = {t: i for i, t in self.n_t2i.items()}
292
+
293
+ # Extract unique edges and triangles
294
+ edgeset = set()
295
+ triset = set()
296
+ for itet in range(self.tets.shape[1]):
297
+ i1, i2, i3, i4 = sorted([int(ind) for ind in self.tets[:, itet]])
298
+ edgeset.add((i1, i2))
299
+ edgeset.add((i1, i3))
300
+ edgeset.add((i1, i4))
301
+ edgeset.add((i2, i3))
302
+ edgeset.add((i2, i4))
303
+ edgeset.add((i3, i4))
304
+ triset.add((i1,i2,i3))
305
+ triset.add((i1,i2,i4))
306
+ triset.add((i1,i3,i4))
307
+ triset.add((i2,i3,i4))
308
+
309
+ # Edges are effectively Randomly sorted
310
+ # It contains index pairs of vertices edge 1 = (ev1, ev2) etc.
311
+ # Same for traingles
312
+ self.edges = np.array(sorted(list(edgeset))).T
313
+ self.tris = np.array(sorted(list(triset))).T
314
+
315
+ self.tri_centers = (self.nodes[:,self.tris[0,:]] + self.nodes[:,self.tris[1,:]] + self.nodes[:,self.tris[2,:]]) / 3
316
+ def _hash(ints):
317
+ return tuple(sorted([int(x) for x in ints]))
318
+
319
+ # Map edge index tuples to edge indices
320
+ # This mapping tells which characteristic index pair (4,3) maps to which edge
321
+ self.inv_edges = {(int(self.edges[0,i]), int(self.edges[1,i])): i for i in range(self.edges.shape[1])}
322
+ self.inv_tris = {_hash((self.tris[0,i], self.tris[1,i], self.tris[2,i])): i for i in range(self.tris.shape[1])}
323
+ self.inv_tets = {_hash((self.tets[0,i], self.tets[1,i], self.tets[2,i], self.tets[3,i])): i for i in range(self.tets.shape[1])}
324
+
325
+ # Tet links
326
+
327
+ self.tet_to_edge = np.zeros((6, self.tets.shape[1]), dtype=int)-99999
328
+ self.tet_to_edge_sign = np.zeros((6, self.tets.shape[1]), dtype=int)-999999
329
+ self.tet_to_tri = np.zeros((4, self.tets.shape[1]), dtype=int)-99999
330
+ self.tet_to_tri_sign = np.zeros((4, self.tets.shape[1]), dtype=int)-999999
331
+
332
+ tri_to_tet = defaultdict(list)
333
+ for itet in range(self.tets.shape[1]):
334
+ edge_ids = [self.get_edge(self.tets[i-1,itet],self.tets[j-1,itet]) for i,j in zip([1, 1, 1, 2, 4, 3], [2, 3, 4, 3, 2, 4])]
335
+ id_signs = [self.get_edge_sign(self.tets[i-1,itet],self.tets[j-1,itet]) for i,j in zip([1, 1, 1, 2, 4, 3], [2, 3, 4, 3, 2, 4])]
336
+ self.tet_to_edge[:,itet] = edge_ids
337
+ self.tet_to_edge_sign[:,itet] = id_signs
338
+ self.tet_to_tri[:,itet] = [self.get_tri(self.tets[i-1,itet],self.tets[j-1,itet],self.tets[k-1,itet]) for i,j,k in zip([1, 1, 1, 2], [2, 3, 4, 3], [3, 4, 2, 4])]
339
+
340
+
341
+ self.tet_to_tri_sign[0,itet] = tri_ordering(self.tets[0,itet], self.tets[1,itet], self.tets[2,itet])
342
+ self.tet_to_tri_sign[1,itet] = tri_ordering(self.tets[0,itet], self.tets[2,itet], self.tets[3,itet])
343
+ self.tet_to_tri_sign[2,itet] = tri_ordering(self.tets[0,itet], self.tets[3,itet], self.tets[1,itet])
344
+ self.tet_to_tri_sign[3,itet] = tri_ordering(self.tets[1,itet], self.tets[2,itet], self.tets[3,itet])
345
+
346
+ tri_to_tet[self.tet_to_tri[0, itet]].append(itet)
347
+ tri_to_tet[self.tet_to_tri[1, itet]].append(itet)
348
+ tri_to_tet[self.tet_to_tri[2, itet]].append(itet)
349
+ tri_to_tet[self.tet_to_tri[3, itet]].append(itet)
350
+
351
+ # Tri links
352
+ self.tri_to_tet = np.zeros((2, self.tris.shape[1]), dtype=int)-1
353
+ for itri in range(self.tris.shape[1]):
354
+ tets = tri_to_tet[itri]
355
+ self.tri_to_tet[:len(tets), itri] = tets
356
+
357
+ _, tet_tags, tet_node_tags = gmsh.model.mesh.get_elements(2)
358
+
359
+ # The algorithm assumes that only one domain tag is returned in this function.
360
+ # Hence the use of tri_node_tags[0] in the next line. If domains are missing.
361
+ # Make sure to combine all the entries in the tri-node-tags list
362
+ tet_node_tags = [self.n_t2i[int(t)] for t in tet_node_tags[0]]
363
+ tet_tags = np.squeeze(np.array(tet_tags))
364
+
365
+ self.tri_i2t = {self.get_tri(*self.tris[:,i]): int(t) for i, t in enumerate(tet_tags)}
366
+ self.tri_t2i = {t: i for i, t in self.tri_i2t.items()}
367
+
368
+ self.tri_to_edge = np.ndarray((3, self.tris.shape[1]), dtype=int)
369
+ self.tri_to_edge_sign = np.ndarray((3, self.tris.shape[1]), dtype=int)
370
+ self.edge_to_tri = defaultdict(list)
371
+
372
+ for itri in range(self.tris.shape[1]):
373
+ i1, i2, i3 = self.tris[:, itri]
374
+ ie1 = self.get_edge(i1,i2)
375
+ ie2 = self.get_edge(i2,i3)
376
+ ie3 = self.get_edge(i1,i3)
377
+ self.tri_to_edge[:,itri] = [ie1, ie2, ie3]
378
+ self.tri_to_edge_sign[:,itri] = [self.get_edge_sign(i1,i2), self.get_edge_sign(i2,i3), self.get_edge_sign(i3,i1)]
379
+ self.edge_to_tri[ie1].append(itri)
380
+ self.edge_to_tri[ie2].append(itri)
381
+ self.edge_to_tri[ie3].append(itri)
382
+
383
+ self.node_to_edge = defaultdict(list)
384
+ for eid in range(self.n_edges):
385
+ v1, v2 = self.edges[0, eid], self.edges[1, eid]
386
+ self.node_to_edge[v1].append(eid)
387
+ self.node_to_edge[v2].append(eid)
388
+
389
+ self.node_to_edge = {key: sorted(list(set(val))) for key, val in self.node_to_edge.items()}
390
+
391
+ ## Quantities
392
+
393
+ self.edge_centers = (self.nodes[:,self.edges[0,:]] + self.nodes[:,self.edges[1,:]]) / 2
394
+ self.edge_lengths = np.sqrt(np.sum((self.nodes[:,self.edges[0,:]] - self.nodes[:,self.edges[1,:]])**2, axis=0))
395
+ 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])])
396
+
397
+
398
+ ## Tag bindings
399
+ face_dimtags = gmsh.model.get_entities(2)
400
+ for d,t in face_dimtags:
401
+ domain_tag, f_tags, node_tags = gmsh.model.mesh.get_elements(2, t)
402
+ node_tags = [self.n_t2i[int(t)] for t in node_tags[0]]
403
+ self.ftag_to_node[t] = node_tags
404
+ node_tags = np.squeeze(np.array(node_tags)).reshape(-1,3).T
405
+ self.ftag_to_tri[t] = [self.get_tri(node_tags[0,i], node_tags[1,i], node_tags[2,i]) for i in range(node_tags.shape[1])]
406
+
407
+ vol_dimtags = gmsh.model.get_entities(3)
408
+ for d,t in vol_dimtags:
409
+ domain_tag, v_tags, node_tags = gmsh.model.mesh.get_elements(3, t)
410
+ node_tags = [self.n_t2i[int(t)] for t in node_tags[0]]
411
+ node_tags = np.squeeze(np.array(node_tags)).reshape(-1,4).T
412
+ self.vtag_to_tet[t] = [self.get_tet(node_tags[0,i], node_tags[1,i], node_tags[2,i], node_tags[3,i]) for i in range(node_tags.shape[1])]
413
+
414
+ self.defined = True
415
+
416
+
417
+ ## Higher order functions
418
+
419
+ def _derive_node_map(self, bc: Periodic) -> tuple[dict[int, int], np.ndarray, np.ndarray]:
420
+ """Computes an old to new node index mapping that preserves global sorting
421
+
422
+ Since basis function field direction is based on the order of indices in tetrahedron
423
+ for periodic boundaries it is important that all triangles and edges in each source
424
+ face are in the same order as the target face. This method computes the mapping for the
425
+ secondary face nodes
426
+
427
+ Args:
428
+ bc (Periodic): The Periodic boundary condition
429
+
430
+ Returns:
431
+ tuple[dict[int, int], np.ndarray, np.ndarray]: The node index mapping and the node index arrays
432
+ """
433
+
434
+ def gen_key(coord, mult):
435
+ return tuple([int(round(c*mult)) for c in coord])
436
+
437
+ ftag_to_node = dict()
438
+ face_dimtags = gmsh.model.get_entities(2)
439
+
440
+ node_ids_1 = []
441
+ node_ids_2 = []
442
+
443
+ for d,t in face_dimtags:
444
+ domain_tag, f_tags, node_tags = gmsh.model.mesh.get_elements(2, t)
445
+ node_tags = [self.n_t2i[int(t)] for t in node_tags[0]]
446
+ if t in bc.face1.tags:
447
+ node_ids_1.extend(node_tags)
448
+ if t in bc.face2.tags:
449
+ node_ids_2.extend(node_tags)
450
+ ftag_to_node[t] = node_tags
451
+
452
+ all_node_ids = np.unique(np.array(node_ids_1 + node_ids_2))
453
+ dsmin = shortest_distance(self.nodes[:,all_node_ids])
454
+
455
+ node_ids_1 = sorted(list(set(node_ids_1)))
456
+ node_ids_2 = sorted(list(set(node_ids_2)))
457
+ dv = np.array(bc.dv)
458
+ nodemapdict = defaultdict(lambda: [None, None])
459
+
460
+ mult = int(10**(-np.round(np.log10(dsmin))+2))
461
+
462
+ for i1, i2 in zip(node_ids_1, node_ids_2):
463
+ nodemapdict[gen_key(self.nodes[:,i1], mult)][0] = i1
464
+ nodemapdict[gen_key(self.nodes[:,i2]-dv, mult)][1] = i2
465
+
466
+ nodemap = {i1: i2 for i1, i2 in nodemapdict.values()}
467
+
468
+ node_ids_2_unsorted = [nodemap[i] for i in sorted(node_ids_1)]
469
+ node_ids_2_sorted = sorted(node_ids_2_unsorted)
470
+ conv_map = {i1: i2 for i1, i2 in zip(node_ids_2_unsorted, node_ids_2_sorted)}
471
+ return conv_map, np.array(node_ids_2_unsorted), np.array(node_ids_2_sorted)
472
+
473
+
474
+ def retreive(self, material_selector: Callable, volumes: list[GeoVolume]) -> np.ndarray:
475
+ '''Retrieve the material properties of the geometry'''
476
+ arry = np.zeros((3,3,self.n_tets,), dtype=np.complex128)
477
+
478
+ xs = self.centers[0,:]
479
+ ys = self.centers[1,:]
480
+ zs = self.centers[2,:]
481
+
482
+ for volume in sorted(volumes, key=lambda x: x._priority):
483
+
484
+ for dimtag in volume.dimtags:
485
+ etype, etag_list, ntags = gmsh.model.mesh.get_elements(*dimtag)
486
+ for etags in etag_list:
487
+ tet_ids = [self.tet_t2i[t] for t in etags]
488
+
489
+ value = material_selector(volume.material, xs[tet_ids], ys[tet_ids], zs[tet_ids])
490
+ arry[:,:,tet_ids] = value
491
+ return arry
492
+
493
+ def plot_gmsh(self) -> None:
494
+ gmsh.fltk.run()
495
+
496
+ def find_edge_groups(self, edge_ids: np.ndarray) -> dict:
497
+ """
498
+ Find the groups of edges in the mesh.
499
+
500
+ Split an edge list into sets (islands) whose vertices are mutually connected.
501
+
502
+ Parameters
503
+ ----------
504
+ edges : np.ndarray, shape (2, N)
505
+ edges[0, i] and edges[1, i] are the two vertex indices of edge *i*.
506
+ The array may contain any (hashable) integer vertex labels, in any order.
507
+
508
+ Returns
509
+ -------
510
+ List[Tuple[int, ...]]
511
+ A list whose *k*‑th element is a `tuple` with the (zero‑based) **edge IDs**
512
+ that belong to the *k*‑th connected component. Ordering is:
513
+ • components appear in the order in which their first edge is met,
514
+ • edge IDs inside each tuple are sorted increasingly.
515
+
516
+ Notes
517
+ -----
518
+ * Only the connectivity of the supplied edges is considered.
519
+ In particular, vertices that never occur in `edges` do **not** create extra
520
+ components.
521
+ * Runtime is *O*(N + V), with N = number of edges, V = number of
522
+ distinct vertices. No external libraries are needed.
523
+ """
524
+ edges = self.edges[:,edge_ids]
525
+ if edges.ndim != 2 or edges.shape[0] != 2:
526
+ raise ValueError("`edges` must have shape (2, N)")
527
+
528
+ #n_edges: int = edges.shape[1]
529
+
530
+ # --- build “vertex ⇒ incident edge IDs” map ------------------------------
531
+ vert2edges = defaultdict(list)
532
+ for eid in edge_ids:
533
+ v1, v2 = self.edges[0, eid], self.edges[1, eid]
534
+ vert2edges[v1].append(eid)
535
+ vert2edges[v2].append(eid)
536
+
537
+ groups = []
538
+
539
+ ungrouped = set(edge_ids)
540
+
541
+ group = [edge_ids[0],]
542
+ ungrouped.remove(edge_ids[0])
543
+
544
+ while True:
545
+ new_edges = set()
546
+ for edge in group:
547
+ v1, v2 = self.edges[0, edge], self.edges[1, edge]
548
+ new_edges.update(set(vert2edges[v1]))
549
+ new_edges.update(set(vert2edges[v2]))
550
+
551
+ new_edges = new_edges.intersection(ungrouped)
552
+ if len(new_edges) == 0:
553
+ groups.append(tuple(sorted(group)))
554
+ if len(ungrouped) == 0:
555
+ break
556
+ group = [ungrouped.pop(),]
557
+ else:
558
+ group += list(new_edges)
559
+ ungrouped.difference_update(new_edges)
560
+
561
+ return groups
562
+
563
+ def boundary_surface(self,
564
+ face_tags: Union[int, list[int]],
565
+ origin: tuple[float, float, float] = None) -> SurfaceMesh:
566
+ tri_ids = self.get_triangles(face_tags)
567
+ if origin is None:
568
+ nodes = self.nodes[:,self.get_nodes(face_tags)]
569
+ x0 = np.mean(nodes[0,:])
570
+ y0 = np.mean(nodes[1,:])
571
+ z0 = np.mean(nodes[2,:])
572
+ origin = (x0, y0, z0)
573
+
574
+ return SurfaceMesh(self, tri_ids, origin)
575
+
576
+ class SurfaceMesh:
577
+
578
+ def __init__(self,
579
+ original: Mesh3D,
580
+ tri_ids: np.ndarray,
581
+ origin: tuple[float, float, float]):
582
+
583
+ ## Compute derived mesh properties
584
+ tris = original.tris[:, tri_ids]
585
+ unique_nodes = np.sort(np.unique(tris.flatten()))
586
+ new_ids = np.arange(unique_nodes.shape[0])
587
+ old_to_new_node_id_map = {a: b for a,b in zip(unique_nodes, new_ids)}
588
+ new_tris = np.array([[old_to_new_node_id_map[tris[i,j]] for i in range(3)] for j in range(tris.shape[1])]).T
589
+
590
+
591
+ ### Store information
592
+ self._tri_ids = tri_ids
593
+ self._origin = origin
594
+
595
+ self.original_tris: np.ndarray = original.tris
596
+
597
+ self.old_new_node_map: dict[int,int] = old_to_new_node_id_map
598
+ self.original: Mesh3D = original
599
+ self._alignment_origin: np.ndarray = np.array(origin).astype(np.float64)
600
+ self.nodes: np.ndarray = original.nodes[:, unique_nodes]
601
+ self.tris: np.ndarray = new_tris
602
+
603
+ ## initialize derived
604
+ self.edge_centers: np.ndarray = None
605
+ self.edge_tris: np.ndarray = None
606
+ self.n_nodes = self.nodes.shape[1]
607
+ self.n_tris = self.tris.shape[1]
608
+ self.n_edges = None
609
+ self.areas: np.ndarray = None
610
+ self.normals: np.ndarray = None
611
+
612
+ # Generate derived
613
+ self.update()
614
+
615
+ def copy(self) -> SurfaceMesh:
616
+ return SurfaceMesh(self.original, self._tri_ids, self._origin)
617
+
618
+ def flip(self, ax: str) -> SurfaceMesh:
619
+ if ax.lower()=='x':
620
+ self.flipX()
621
+ if ax.lower()=='y':
622
+ self.flipY()
623
+ if ax.lower()=='z':
624
+ self.flipZ()
625
+ #self.tris[(0,1),:] = self.tris[(1,0),:]
626
+
627
+ def flipX(self) -> SurfaceMesh:
628
+ self.nodes[0,:] = -self.nodes[0,:]
629
+ self.normals[0,:] = -self.normals[0,:]
630
+ self.edge_centers[0,:] = -self.edge_centers[0,:]
631
+
632
+ def flipY(self) -> SurfaceMesh:
633
+ self.nodes[1,:] = -self.nodes[1,:]
634
+ self.normals[1,:] = -self.normals[1,:]
635
+ self.edge_centers[1,:] = -self.edge_centers[1,:]
636
+
637
+ def flipZ(self) -> SurfaceMesh:
638
+ self.nodes[2,:] = -self.nodes[2,:]
639
+ self.normals[2,:] = -self.normals[2,:]
640
+ self.edge_centers[2,:] = -self.edge_centers[2,:]
641
+
642
+ def from_source_tri(self, triid: int) -> int | None:
643
+ ''' Returns a triangle index from the old mesh to the new mesh.'''
644
+ i1in = self.original.tris[0,triid]
645
+ i2in = self.original.tris[1,triid]
646
+ i3in = self.original.tris[2,triid]
647
+ i1 = self.old_new_node_map.get(i1in,None)
648
+ i2 = self.old_new_node_map.get(i2in,None)
649
+ i3 = self.old_new_node_map.get(i3in,None)
650
+ if i1 is None or i2 is None or i3 is None:
651
+ return None
652
+ return self.get_tri(i1, i2, i3)
653
+
654
+ def from_source_edge(self, edgeid: int) -> int | None:
655
+ ''' Returns an edge index form the old mesh to the new mesh.'''
656
+ i1 = self.old_new_node_map.get(self.original.edges[0,edgeid],None)
657
+ i2 = self.old_new_node_map.get(self.original.edges[1,edgeid],None)
658
+ if i1 is None or i2 is None:
659
+ return None
660
+ return self.get_edge(i1, i2)
661
+
662
+ def get_edge(self, i1: int, i2: int) -> int:
663
+ '''Return the edge index given the two node indices'''
664
+ if i1==i2:
665
+ raise ValueError("Edge cannot be formed by the same node.")
666
+ search = (min(int(i1),int(i2)), max(int(i1),int(i2)))
667
+ result = self.inv_edges.get(search, None)
668
+ return result
669
+
670
+ def get_edge_sign(self, i1: int, i2: int) -> int:
671
+ '''Return the edge index given the two node indices'''
672
+ if i1==i2:
673
+ raise ValueError("Edge cannot be formed by the same node.")
674
+ if i1 > i2:
675
+ return -1
676
+ return 1
677
+
678
+ def get_tri(self, i1, i2, i3) -> int:
679
+ '''Return the triangle index given the three node indices'''
680
+ return self.inv_tris.get(tuple(sorted((int(i1), int(i2), int(i3)))), None)
681
+
682
+ def update(self) -> None:
683
+ ## First Edges
684
+
685
+ edges = set()
686
+ for i in range(self.n_tris):
687
+ i1, i2, i3 = self.tris[:,i]
688
+ edges.add((i1, i2))
689
+ edges.add((i2, i3))
690
+ edges.add((i1, i3))
691
+
692
+ edgelist = list(edges)
693
+
694
+ self.edges = np.array(edgelist).T
695
+ self.n_edges = self.edges.shape[1]
696
+ self.edge_centers = (self.nodes[:,self.edges[0,:]] + self.nodes[:,self.edges[1,:]])/2
697
+
698
+ ## Mapping from edge pairs to edge index
699
+
700
+ def _hash(ints):
701
+ return tuple(sorted([int(x) for x in ints]))
702
+
703
+ self.inv_edges = {(int(self.edges[0,i]), int(self.edges[1,i])): i for i in range(self.edges.shape[1])}
704
+ self.inv_tris = {_hash((self.tris[0,i], self.tris[1,i], self.tris[2,i])): i for i in range(self.tris.shape[1])}
705
+ ##
706
+ origin = self._alignment_origin
707
+
708
+ self.areas = np.array([area(self.nodes[:,self.tris[0,i]],
709
+ self.nodes[:,self.tris[1,i]],
710
+ self.nodes[:,self.tris[2,i]]) for i in range(self.n_tris)]).T
711
+ self.normals = np.array([outward_normal(
712
+ self.nodes[:,self.tris[0,i]],
713
+ self.nodes[:,self.tris[1,i]],
714
+ self.nodes[:,self.tris[2,i]],
715
+ origin) for i in range(self.n_tris)]).T
716
+
717
+ self.tri_to_edge = np.ndarray((3, self.tris.shape[1]), dtype=int)
718
+ self.edge_to_tri = defaultdict(list)
719
+
720
+ for itri in range(self.tris.shape[1]):
721
+ i1, i2, i3 = self.tris[:, itri]
722
+ ie1 = self.get_edge(i1,i2)
723
+ ie2 = self.get_edge(i2,i3)
724
+ ie3 = self.get_edge(i1,i3)
725
+ self.tri_to_edge[:,itri] = [ie1, ie2, ie3]
726
+
727
+ @property
728
+ def exyz(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
729
+ return self.edge_centers[0,:], self.edge_centers[1,:], self.edge_centers[2,:]
730
+