emerge 0.4.6__py3-none-any.whl → 0.4.8__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.
- emerge/__init__.py +54 -0
- emerge/__main__.py +5 -0
- emerge/_emerge/__init__.py +42 -0
- emerge/_emerge/bc.py +197 -0
- emerge/_emerge/coord.py +119 -0
- emerge/_emerge/cs.py +523 -0
- emerge/_emerge/dataset.py +36 -0
- emerge/_emerge/elements/__init__.py +19 -0
- emerge/_emerge/elements/femdata.py +212 -0
- emerge/_emerge/elements/index_interp.py +64 -0
- emerge/_emerge/elements/legrange2.py +172 -0
- emerge/_emerge/elements/ned2_interp.py +645 -0
- emerge/_emerge/elements/nedelec2.py +140 -0
- emerge/_emerge/elements/nedleg2.py +217 -0
- emerge/_emerge/geo/__init__.py +24 -0
- emerge/_emerge/geo/horn.py +107 -0
- emerge/_emerge/geo/modeler.py +449 -0
- emerge/_emerge/geo/operations.py +254 -0
- emerge/_emerge/geo/pcb.py +1244 -0
- emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
- emerge/_emerge/geo/pcb_tools/macro.py +79 -0
- emerge/_emerge/geo/pmlbox.py +204 -0
- emerge/_emerge/geo/polybased.py +529 -0
- emerge/_emerge/geo/shapes.py +427 -0
- emerge/_emerge/geo/step.py +77 -0
- emerge/_emerge/geo2d.py +86 -0
- emerge/_emerge/geometry.py +510 -0
- emerge/_emerge/howto.py +214 -0
- emerge/_emerge/logsettings.py +5 -0
- emerge/_emerge/material.py +118 -0
- emerge/_emerge/mesh3d.py +730 -0
- emerge/_emerge/mesher.py +339 -0
- emerge/_emerge/mth/common_functions.py +33 -0
- emerge/_emerge/mth/integrals.py +71 -0
- emerge/_emerge/mth/optimized.py +357 -0
- emerge/_emerge/periodic.py +263 -0
- emerge/_emerge/physics/__init__.py +0 -0
- emerge/_emerge/physics/microwave/__init__.py +1 -0
- emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
- emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
- emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
- emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
- emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
- emerge/_emerge/physics/microwave/periodic.py +82 -0
- emerge/_emerge/physics/microwave/port_functions.py +53 -0
- emerge/_emerge/physics/microwave/sc.py +175 -0
- emerge/_emerge/physics/microwave/simjob.py +147 -0
- emerge/_emerge/physics/microwave/sparam.py +138 -0
- emerge/_emerge/physics/microwave/touchstone.py +140 -0
- emerge/_emerge/plot/__init__.py +0 -0
- emerge/_emerge/plot/display.py +394 -0
- emerge/_emerge/plot/grapher.py +93 -0
- emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
- emerge/_emerge/plot/pyvista/__init__.py +1 -0
- emerge/_emerge/plot/pyvista/display.py +931 -0
- emerge/_emerge/plot/pyvista/display_settings.py +24 -0
- emerge/_emerge/plot/simple_plots.py +551 -0
- emerge/_emerge/plot.py +225 -0
- emerge/_emerge/projects/__init__.py +0 -0
- emerge/_emerge/projects/_gen_base.txt +32 -0
- emerge/_emerge/projects/_load_base.txt +24 -0
- emerge/_emerge/projects/generate_project.py +40 -0
- emerge/_emerge/selection.py +596 -0
- emerge/_emerge/simmodel.py +444 -0
- emerge/_emerge/simulation_data.py +411 -0
- emerge/_emerge/solver.py +993 -0
- emerge/_emerge/system.py +54 -0
- emerge/cli.py +19 -0
- emerge/lib.py +57 -0
- emerge/plot.py +1 -0
- emerge/pyvista.py +1 -0
- {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
- emerge-0.4.8.dist-info/RECORD +78 -0
- emerge-0.4.8.dist-info/entry_points.txt +2 -0
- emerge-0.4.6.dist-info/RECORD +0 -4
- emerge-0.4.6.dist-info/entry_points.txt +0 -2
- {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/WHEEL +0 -0
emerge/_emerge/mesh3d.py
ADDED
|
@@ -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
|
+
|