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.
- emerge/__init__.py +14 -14
- 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 +1 -1
- emerge/plot.py +1 -1
- {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/METADATA +7 -6
- emerge-0.4.9.dist-info/RECORD +78 -0
- emerge-0.4.9.dist-info/entry_points.txt +2 -0
- emerge-0.4.7.dist-info/RECORD +0 -9
- emerge-0.4.7.dist-info/entry_points.txt +0 -2
- {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
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 scipy.spatial import ConvexHull
|
|
22
|
+
from .cs import Axis, CoordinateSystem, _parse_vector, Plane
|
|
23
|
+
from typing import Callable, TypeVar
|
|
24
|
+
|
|
25
|
+
def align_rectangle_frame(pts3d: np.ndarray, normal: np.ndarray) -> dict[str, np.ndarray]:
|
|
26
|
+
"""Tries to find a rectangle as convex-hull of a set of points with a given normal vector.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
pts3d (np.ndarray): The points (N,3)
|
|
30
|
+
normal (np.ndarray): The normal vector.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
dict[str, np.ndarray]: The output data
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# 1. centroid
|
|
37
|
+
Omat = np.squeeze(np.mean(pts3d, axis=0))
|
|
38
|
+
|
|
39
|
+
# 2. build e_x, e_y
|
|
40
|
+
n = np.squeeze(normal/np.linalg.norm(normal))
|
|
41
|
+
seed = np.array([1.,0.,0.])
|
|
42
|
+
if abs(seed.dot(n)) > 0.9:
|
|
43
|
+
seed = np.array([0.,1.,0.])
|
|
44
|
+
e_x = seed - n*(seed.dot(n))
|
|
45
|
+
e_x /= np.linalg.norm(e_x)
|
|
46
|
+
e_y = np.cross(n, e_x)
|
|
47
|
+
|
|
48
|
+
# 3. project into 2D
|
|
49
|
+
pts2d = np.vstack([[(p-Omat).dot(e_x), (p-Omat).dot(e_y)] for p in pts3d])
|
|
50
|
+
|
|
51
|
+
# 4. convex hull
|
|
52
|
+
hull = ConvexHull(pts2d)
|
|
53
|
+
hull_pts = pts2d[hull.vertices]
|
|
54
|
+
|
|
55
|
+
# 5. rotating calipers: find min-area rectangle
|
|
56
|
+
best = (None, np.inf, None) # (angle, area, (xmin,xmax,ymin,ymax))
|
|
57
|
+
for i in range(len(hull_pts)):
|
|
58
|
+
p0 = hull_pts[i]
|
|
59
|
+
p1 = hull_pts[(i+1)%len(hull_pts)]
|
|
60
|
+
edge = p1 - p0
|
|
61
|
+
theta = -np.arctan2(edge[1], edge[0]) # rotate so edge aligns with +X
|
|
62
|
+
R = np.array([[np.cos(theta), -np.sin(theta)],
|
|
63
|
+
[np.sin(theta), np.cos(theta)]])
|
|
64
|
+
rot = hull_pts.dot(R.T)
|
|
65
|
+
xmin, ymin = rot.min(axis=0)
|
|
66
|
+
xmax, ymax = rot.max(axis=0)
|
|
67
|
+
area = (xmax-xmin)*(ymax-ymin)
|
|
68
|
+
if area < best[1]:
|
|
69
|
+
best = (theta, area, (xmin,xmax,ymin,ymax), R)
|
|
70
|
+
|
|
71
|
+
theta, _, (xmin,xmax,ymin,ymax), R = best
|
|
72
|
+
|
|
73
|
+
# 6. rectangle axes in 3D
|
|
74
|
+
u = np.cos(-theta)*e_x + np.sin(-theta)*e_y
|
|
75
|
+
v = -np.sin(-theta)*e_x + np.cos(-theta)*e_y
|
|
76
|
+
|
|
77
|
+
# corner points in 3D:
|
|
78
|
+
corners = []
|
|
79
|
+
for a in (xmin, xmax):
|
|
80
|
+
for b in (ymin, ymax):
|
|
81
|
+
# back-project to the original 2D frame:
|
|
82
|
+
p2 = np.array([a, b]).dot(R) # rotate back
|
|
83
|
+
P3 = Omat + p2[0]*e_x + p2[1]*e_y
|
|
84
|
+
corners.append(P3)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"origin": Omat,
|
|
88
|
+
"axes": (u, v, n),
|
|
89
|
+
"corners": np.array(corners).reshape(4,3)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
TSelection = TypeVar("TSelection", bound="Selection")
|
|
93
|
+
|
|
94
|
+
# Your custom alphabet
|
|
95
|
+
ALPHABET = (
|
|
96
|
+
"abcdefghijklmnopqrstuvwxyz"
|
|
97
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
98
|
+
"012345%#"
|
|
99
|
+
)
|
|
100
|
+
# Map char → int and int → char
|
|
101
|
+
CHAR_TO_VAL = {ch: i for i, ch in enumerate(ALPHABET)}
|
|
102
|
+
VAL_TO_CHAR = {i: ch for i, ch in enumerate(ALPHABET)}
|
|
103
|
+
|
|
104
|
+
def encode_data(values: tuple[float,...]) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Convert a tuple of floats into a custom base64-like encoded string
|
|
107
|
+
using 16-bit float representation and a 64-character alphabet.
|
|
108
|
+
"""
|
|
109
|
+
# Convert floats to 16-bit half-floats
|
|
110
|
+
arr = np.array(values, dtype=np.float16)
|
|
111
|
+
# Get raw bytes
|
|
112
|
+
byte_data = arr.tobytes()
|
|
113
|
+
# Convert bytes to a bitstring
|
|
114
|
+
bitstring = ""
|
|
115
|
+
for byte in byte_data:
|
|
116
|
+
bitstring += f"{byte:08b}"
|
|
117
|
+
|
|
118
|
+
# Pad the bitstring to a multiple of 6 bits
|
|
119
|
+
pad_len = (6 - len(bitstring) % 6) % 6
|
|
120
|
+
bitstring += "0" * pad_len
|
|
121
|
+
|
|
122
|
+
# Encode 6 bits at a time
|
|
123
|
+
encoded = ""
|
|
124
|
+
for i in range(0, len(bitstring), 6):
|
|
125
|
+
chunk = bitstring[i:i+6]
|
|
126
|
+
val = int(chunk, 2)
|
|
127
|
+
encoded += VAL_TO_CHAR[val]
|
|
128
|
+
|
|
129
|
+
# Optionally store how many pad bits we added
|
|
130
|
+
# so we can remove them during decoding.
|
|
131
|
+
# We'll prepend it as one character encoding pad_len (0-5).
|
|
132
|
+
encoded = VAL_TO_CHAR[pad_len] + encoded
|
|
133
|
+
return encoded
|
|
134
|
+
|
|
135
|
+
def decode_data(encoded: str) -> tuple[float,...]:
|
|
136
|
+
"""
|
|
137
|
+
Decode a string produced by floats_to_custom_string
|
|
138
|
+
back into a tuple of float64 values.
|
|
139
|
+
"""
|
|
140
|
+
# The first character encodes how many zero bits were padded
|
|
141
|
+
pad_char = encoded[0]
|
|
142
|
+
pad_len = CHAR_TO_VAL[pad_char]
|
|
143
|
+
data = encoded[1:]
|
|
144
|
+
|
|
145
|
+
# Convert each char back to 6 bits
|
|
146
|
+
bitstring = ""
|
|
147
|
+
for ch in data:
|
|
148
|
+
val = CHAR_TO_VAL[ch]
|
|
149
|
+
bitstring += f"{val:06b}"
|
|
150
|
+
|
|
151
|
+
# Remove any padding bits at the end
|
|
152
|
+
if pad_len > 0:
|
|
153
|
+
bitstring = bitstring[:-pad_len]
|
|
154
|
+
|
|
155
|
+
# Split into bytes
|
|
156
|
+
byte_data = []
|
|
157
|
+
for i in range(0, len(bitstring), 8):
|
|
158
|
+
byte_chunk = bitstring[i:i+8]
|
|
159
|
+
if len(byte_chunk) < 8:
|
|
160
|
+
break
|
|
161
|
+
byte_val = int(byte_chunk, 2)
|
|
162
|
+
byte_data.append(byte_val)
|
|
163
|
+
|
|
164
|
+
byte_array = bytes(byte_data)
|
|
165
|
+
# Recover as float16
|
|
166
|
+
arr = np.frombuffer(byte_array, dtype=np.float16)
|
|
167
|
+
# Convert back to float64 for higher precision
|
|
168
|
+
return tuple(np.array(arr, dtype=np.float64))
|
|
169
|
+
|
|
170
|
+
class Selection:
|
|
171
|
+
"""A generalized class representing a slection of tags.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
dim: int = -1
|
|
175
|
+
def __init__(self, tags: list[int] = None):
|
|
176
|
+
|
|
177
|
+
self._tags: set[int] = {}
|
|
178
|
+
|
|
179
|
+
if tags is not None:
|
|
180
|
+
if not isinstance(tags, (list,set,tuple)):
|
|
181
|
+
raise TypeError(f'Argument tags must be of type list, tuple or set, instead its {type(tags)}')
|
|
182
|
+
self._tags = set(tags)
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def from_dim_tags(dim: int, tags: list[int]) -> DomainSelection | PointSelection | FaceSelection | EdgeSelection:
|
|
186
|
+
if dim==0:
|
|
187
|
+
return PointSelection(tags)
|
|
188
|
+
elif dim==1:
|
|
189
|
+
return EdgeSelection(tags)
|
|
190
|
+
elif dim==2:
|
|
191
|
+
return FaceSelection(tags)
|
|
192
|
+
elif dim==3:
|
|
193
|
+
return DomainSelection(tags)
|
|
194
|
+
raise ValueError(f'Dimension must be 0,1,2 or 3. Not {dim}')
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def tags(self) -> list[int]:
|
|
198
|
+
return list(self._tags)
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def color_rgb(self) -> tuple[int,int,int]:
|
|
202
|
+
return (0.5,0.5,1.0)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def centers(self) -> list[tuple[float, float, float],]:
|
|
206
|
+
return [gmsh.model.occ.get_center_of_mass(self.dim, tag) for tag in self.tags]
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def opacity(self) -> float:
|
|
210
|
+
return 0.6
|
|
211
|
+
####### DUNDER METHODS
|
|
212
|
+
def __repr__(self) -> str:
|
|
213
|
+
return f'{type(self).__name__}({self.tags})'
|
|
214
|
+
|
|
215
|
+
####### PROPERTIES
|
|
216
|
+
@property
|
|
217
|
+
def dimtags(self) -> list[tuple[int,int]]:
|
|
218
|
+
return [(self.dim, tag) for tag in self.tags]
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def center(self) -> np.ndarray | list[np.ndarray]:
|
|
222
|
+
if len(self.tags)==1:
|
|
223
|
+
return gmsh.model.occ.getCenterOfMass(self.dim, self.tags[0])
|
|
224
|
+
else:
|
|
225
|
+
return [gmsh.model.occ.getCenterOfMass(self.dim, tag) for tag in self.tags]
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def points(self) -> np.ndarray | list[np.ndarray]:
|
|
229
|
+
'''A list of 3D coordinates of all nodes comprising the selection.'''
|
|
230
|
+
points = gmsh.model.get_boundary(self.dimtags, recursive=True)
|
|
231
|
+
coordinates = [gmsh.model.getValue(*p, []) for p in points]
|
|
232
|
+
return coordinates
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def bounding_box(self) -> tuple[np.ndarray, np.ndarray]:
|
|
236
|
+
if len(self.tags)==1:
|
|
237
|
+
return gmsh.model.occ.getBoundingBox(self.dim, self.tags[0])
|
|
238
|
+
else:
|
|
239
|
+
minx = miny = minz = 1e10
|
|
240
|
+
maxx = maxy = maxz = -1e10
|
|
241
|
+
for tag in self.tags:
|
|
242
|
+
x0, y0, z0, x1, y1, z1 = gmsh.model.occ.getBoundingBox(self.dim, tag)
|
|
243
|
+
minx = min(minx, x0)
|
|
244
|
+
miny = min(miny, y0)
|
|
245
|
+
minz = min(minz, z0)
|
|
246
|
+
maxx = max(maxx, x1)
|
|
247
|
+
maxy = max(maxy, y1)
|
|
248
|
+
maxz = max(maxz, z1)
|
|
249
|
+
return (minx, miny, minz), (maxx, maxy, maxz)
|
|
250
|
+
|
|
251
|
+
def exclude(self, xyz_excl_function: Callable = lambda x,y,z: True, plane: Plane = None, axis: Axis = None) -> Selection:
|
|
252
|
+
"""Exclude points by evaluating a function(x,y,z)-> bool
|
|
253
|
+
|
|
254
|
+
This modifies the selection such that the selection does not contain elements
|
|
255
|
+
of this selection of which the center of mass is excluded by the exclusion function.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
xyz_excl_function (Callable): A callable for (x,y,z) that returns True if the point should be excluded.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Selection: This Selection modified without the excluded points.
|
|
262
|
+
"""
|
|
263
|
+
include = [~xyz_excl_function(*gmsh.model.occ.getCenterOfMass(*tag)) for tag in self.dimtags]
|
|
264
|
+
|
|
265
|
+
if axis is not None:
|
|
266
|
+
norm = axis.np
|
|
267
|
+
include2 = [abs(gmsh.model.getNormal(tag, np.array([0,0]))@norm)<0.9 for tag in self.tags]
|
|
268
|
+
include = [i1 for i1, i2 in zip(include, include2) if i1 and i2]
|
|
269
|
+
self._tags = [t for incl, t in zip(include, self._tags) if incl]
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
def isolate(self, xyz_excl_function: Callable = lambda x,y,z: True, plane: Plane = None, axis: Axis = None) -> Selection:
|
|
273
|
+
"""Include points by evaluating a function(x,y,z)-> bool
|
|
274
|
+
|
|
275
|
+
This modifies the selection such that the selection does not contain elements
|
|
276
|
+
of this selection of which the center of mass is excluded by the exclusion function.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
xyz_excl_function (Callable): A callable for (x,y,z) that returns True if the point should be excluded.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Selection: This Selection modified without the excluded points.
|
|
283
|
+
"""
|
|
284
|
+
include1 = [xyz_excl_function(*gmsh.model.occ.getCenterOfMass(*tag)) for tag in self.dimtags]
|
|
285
|
+
|
|
286
|
+
if axis is not None:
|
|
287
|
+
norm = axis.np
|
|
288
|
+
include2 = [(gmsh.model.getNormal(tag, np.array([0,0]))@norm)>0.99 for tag in self.tags]
|
|
289
|
+
include1 = [i1 for i1, i2 in zip(include1, include2) if i1 and i2]
|
|
290
|
+
self._tags = [t for incl, t in zip(include1, self._tags) if incl]
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def __operable__(self, other: Selection) -> None:
|
|
294
|
+
if not self.dim == other.dim:
|
|
295
|
+
raise ValueError(f'Selection dimensions must be equal. Trying to operate on dim {self.dim} and {other.dim}')
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
def __add__(self, other: TSelection) -> TSelection:
|
|
299
|
+
self.__operable__(other)
|
|
300
|
+
return Selection.from_dim_tags(self.dim, self._tags + other._tags)
|
|
301
|
+
|
|
302
|
+
def __and__(self, other: TSelection) -> TSelection:
|
|
303
|
+
self.__operable__(other)
|
|
304
|
+
return Selection.from_dim_tags(self.dim, self._tags.intersection(other._tags))
|
|
305
|
+
|
|
306
|
+
def __or__(self, other: TSelection) -> TSelection:
|
|
307
|
+
self.__operable__(other)
|
|
308
|
+
return Selection.from_dim_tags(self.dim, self._tags.union(other._tags))
|
|
309
|
+
|
|
310
|
+
def __sub__(self, other: TSelection) -> TSelection:
|
|
311
|
+
self.__operable__(other)
|
|
312
|
+
return Selection.from_dim_tags(self.dim, self._tags.difference(other.tags))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class PointSelection(Selection):
|
|
317
|
+
"""A Class representing a selection of points.
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
dim: int = 0
|
|
321
|
+
def __init__(self, tags: list[int] = None):
|
|
322
|
+
super().__init__(tags)
|
|
323
|
+
|
|
324
|
+
class EdgeSelection(Selection):
|
|
325
|
+
"""A Class representing a selection of edges.
|
|
326
|
+
|
|
327
|
+
"""
|
|
328
|
+
dim: int = 1
|
|
329
|
+
def __init__(self, tags: list[int] = None):
|
|
330
|
+
super().__init__(tags)
|
|
331
|
+
|
|
332
|
+
class FaceSelection(Selection):
|
|
333
|
+
"""A Class representing a selection of Faces.
|
|
334
|
+
|
|
335
|
+
"""
|
|
336
|
+
dim: int = 2
|
|
337
|
+
def __init__(self, tags: list[int] = None):
|
|
338
|
+
super().__init__(tags)
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def normal(self) -> np.ndarray:
|
|
342
|
+
''' Returns a 3x3 coordinate matrix of the XY + out of plane basis matrix defining the face assuming it can be projected on a flat plane.'''
|
|
343
|
+
ns = [gmsh.model.getNormal(tag, np.array([0,0])) for tag in self.tags]
|
|
344
|
+
return ns[0]
|
|
345
|
+
|
|
346
|
+
def rect_basis(self) -> tuple[CoordinateSystem, tuple[float, float]]:
|
|
347
|
+
''' Returns a dictionary with keys: origin, axes, corners. The axes are the 3D basis vectors of the rectangle. The corners are the 4 corners of the rectangle.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
cs: CoordinateSystem: The coordinate system of the rectangle.
|
|
351
|
+
size: tuple[float, float]: The size of the rectangle (width [m], height[m])
|
|
352
|
+
'''
|
|
353
|
+
if len(self.tags) != 1:
|
|
354
|
+
raise ValueError('rect_basis only works for single face selections')
|
|
355
|
+
|
|
356
|
+
pts3d = self.points
|
|
357
|
+
normal = self.normal
|
|
358
|
+
data = align_rectangle_frame(pts3d, normal)
|
|
359
|
+
plane = data['axes'][:2]
|
|
360
|
+
origin = data['origin']
|
|
361
|
+
|
|
362
|
+
cs = CoordinateSystem(Axis(plane[0]), Axis(plane[1]), Axis(data['axes'][2]), origin)
|
|
363
|
+
|
|
364
|
+
size1 = np.linalg.norm(data['corners'][1] - data['corners'][0])
|
|
365
|
+
size2 = np.linalg.norm(data['corners'][2] - data['corners'][0])
|
|
366
|
+
|
|
367
|
+
if size1>size2:
|
|
368
|
+
cs.swapxy()
|
|
369
|
+
return cs, (size1, size2)
|
|
370
|
+
else:
|
|
371
|
+
return cs, (size2, size1)
|
|
372
|
+
|
|
373
|
+
def sample(self, Npts: int) -> tuple[np.ndarray, np.ndarray, np.ndarray] | list[tuple[np.ndarray, np.ndarray, np.ndarray]]:
|
|
374
|
+
''' Sample the surface at the compiler defined parametric coordinate range.
|
|
375
|
+
This function usually returns a square region that contains the surface.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
--------
|
|
379
|
+
X: np.ndarray
|
|
380
|
+
a NxN numpy array of X coordinates.
|
|
381
|
+
Y: np.ndarray
|
|
382
|
+
a NxN numpy array of Y coordinates.
|
|
383
|
+
Z: np.ndarray
|
|
384
|
+
a NxN numpy array of Z coordinates'''
|
|
385
|
+
coordset = []
|
|
386
|
+
for tag in self.tags:
|
|
387
|
+
tags, coord, param = gmsh.model.mesh.getNodes(2, tag, includeBoundary=True)
|
|
388
|
+
|
|
389
|
+
uss = param[0::2]
|
|
390
|
+
vss = param[1::2]
|
|
391
|
+
|
|
392
|
+
umin = min(uss)
|
|
393
|
+
umax = max(uss)
|
|
394
|
+
vmin = min(vss)
|
|
395
|
+
vmax = max(vss)
|
|
396
|
+
|
|
397
|
+
us = np.linspace(umin, umax, Npts)
|
|
398
|
+
vs = np.linspace(vmin, vmax, Npts)
|
|
399
|
+
|
|
400
|
+
U, V = np.meshgrid(us, vs, indexing='ij')
|
|
401
|
+
|
|
402
|
+
shp = U.shape
|
|
403
|
+
|
|
404
|
+
uax = U.flatten()
|
|
405
|
+
vax = V.flatten()
|
|
406
|
+
|
|
407
|
+
pcoords = np.zeros((2*uax.shape[0],))
|
|
408
|
+
|
|
409
|
+
pcoords[0::2] = uax
|
|
410
|
+
pcoords[1::2] = vax
|
|
411
|
+
|
|
412
|
+
coords = gmsh.model.getValue(2, tag, pcoords).reshape(-1,3).T
|
|
413
|
+
|
|
414
|
+
coordset.append((coords[0,:].reshape(shp),
|
|
415
|
+
coords[1,:].reshape(shp),
|
|
416
|
+
coords[2,:].reshape(shp)))
|
|
417
|
+
|
|
418
|
+
X = [c[0] for c in coordset]
|
|
419
|
+
Y = [c[1] for c in coordset]
|
|
420
|
+
Z = [c[2] for c in coordset]
|
|
421
|
+
if len(X) == 1:
|
|
422
|
+
X = X[0]
|
|
423
|
+
Y = Y[0]
|
|
424
|
+
Z = Z[0]
|
|
425
|
+
return X, Y, Z
|
|
426
|
+
|
|
427
|
+
class DomainSelection(Selection):
|
|
428
|
+
"""A Class representing a selection of domains.
|
|
429
|
+
|
|
430
|
+
"""
|
|
431
|
+
dim: int = 3
|
|
432
|
+
def __init__(self, tags: list[int] = None):
|
|
433
|
+
super().__init__(tags)
|
|
434
|
+
|
|
435
|
+
SELECT_CLASS: dict[int, type[Selection]] = {
|
|
436
|
+
0: PointSelection,
|
|
437
|
+
1: EdgeSelection,
|
|
438
|
+
2: FaceSelection,
|
|
439
|
+
3: DomainSelection
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
######## SELECTOR
|
|
443
|
+
|
|
444
|
+
class Selector:
|
|
445
|
+
"""A class instance with convenient methods to generate selections using method chaining.
|
|
446
|
+
|
|
447
|
+
Use the specific properties and functions in a "language" like way to make selections.
|
|
448
|
+
|
|
449
|
+
To specify what to select, use the .node, .edge, .face or .domain property.
|
|
450
|
+
These properties return the Selector after which you can say how to execute a selection.
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
"""
|
|
454
|
+
def __init__(self):
|
|
455
|
+
self._current_dim: int = -1
|
|
456
|
+
|
|
457
|
+
## DIMENSION CHAIN
|
|
458
|
+
@property
|
|
459
|
+
def node(self) -> Selector:
|
|
460
|
+
self._current_dim = 0
|
|
461
|
+
return self
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def edge(self) -> Selector:
|
|
465
|
+
self._current_dim = 1
|
|
466
|
+
return self
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def face(self) -> Selector:
|
|
470
|
+
self._current_dim = 2
|
|
471
|
+
return self
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def domain(self) -> Selector:
|
|
475
|
+
self._current_dim = 3
|
|
476
|
+
return self
|
|
477
|
+
|
|
478
|
+
def near(self,
|
|
479
|
+
x: float,
|
|
480
|
+
y: float,
|
|
481
|
+
z: float = 0) -> Selection | PointSelection | EdgeSelection | FaceSelection | DomainSelection:
|
|
482
|
+
"""Returns a selection of the releative dimeions by which of the instances is most proximate to a coordinate.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
x (float): The X-coordinate
|
|
486
|
+
y (float): The Y-coordinate
|
|
487
|
+
z (float, optional): The Z-coordinate. Defaults to 0.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Selection | PointSelection | EdgeSelection | FaceSelection | DomainSelection: The resultant selection.
|
|
491
|
+
"""
|
|
492
|
+
dimtags = gmsh.model.getEntities(self._current_dim)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
dists = [np.linalg.norm(np.array([x,y,z]) - gmsh.model.occ.getCenterOfMass(*tag)) for tag in dimtags]
|
|
496
|
+
index_of_closest = np.argmin(dists)
|
|
497
|
+
|
|
498
|
+
return SELECT_CLASS[self._current_dim]([dimtags[index_of_closest][1],])
|
|
499
|
+
|
|
500
|
+
def inlayer(self,
|
|
501
|
+
x: float,
|
|
502
|
+
y: float,
|
|
503
|
+
z: float,
|
|
504
|
+
vector: tuple[float, float, float] | np.ndarray | Axis) -> FaceSelection | EdgeSelection | DomainSelection:
|
|
505
|
+
'''Returns a list of selections that are in the layer defined by the plane normal vector and the point (x,y,z)
|
|
506
|
+
|
|
507
|
+
The layer is specified by two infinite planes normal to the provided vector. The first plane is originated
|
|
508
|
+
at the provided origin. The second plane is placed such that it contains the point origin+vector.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
x (float): The X-coordinate from which to select
|
|
512
|
+
y (float): The Y-coordinate from which to select
|
|
513
|
+
z (float): The Z-coordinate from which to select
|
|
514
|
+
vector (np.ndarray, tuple, Axis): A vector with length in (meters) originating at the origin.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Selection | PointSelection | EdgeSelection | FaceSelection | DomainSelection: The resultant selection.
|
|
518
|
+
|
|
519
|
+
'''
|
|
520
|
+
vector = _parse_vector(vector)
|
|
521
|
+
|
|
522
|
+
dimtags = gmsh.model.getEntities(self._current_dim)
|
|
523
|
+
|
|
524
|
+
coords = [gmsh.model.occ.getCenterOfMass(*tag) for tag in dimtags]
|
|
525
|
+
|
|
526
|
+
L = np.linalg.norm(vector)
|
|
527
|
+
vector = vector / L
|
|
528
|
+
|
|
529
|
+
output = []
|
|
530
|
+
for i, c in enumerate(coords):
|
|
531
|
+
c_local = c - np.array([x,y,z])
|
|
532
|
+
if 0 < np.dot(vector, c_local) < L:
|
|
533
|
+
output.append(dimtags[i][1])
|
|
534
|
+
return SELECT_CLASS[self._current_dim](output)
|
|
535
|
+
|
|
536
|
+
def inplane(self,
|
|
537
|
+
x: float,
|
|
538
|
+
y: float,
|
|
539
|
+
z: float,
|
|
540
|
+
nx: float,
|
|
541
|
+
ny: float,
|
|
542
|
+
nz: float,
|
|
543
|
+
tolerance: float = 1e-6) -> FaceSelection:
|
|
544
|
+
"""Returns a FaceSelection for all faces that lie in a provided infinite plane
|
|
545
|
+
specified by an origin plus a plane normal vector.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
x (float): The plane origin X-coordinate
|
|
549
|
+
y (float): The plane origin Y-coordinate
|
|
550
|
+
z (float): The plane origin Z-coordinate
|
|
551
|
+
nx (float): The plane normal X-component
|
|
552
|
+
ny (float): The plane normal Y-component
|
|
553
|
+
nz (float): The plane normal Z-component
|
|
554
|
+
tolerance (float, optional): An in plane tolerance (displacement and normal dot product). Defaults to 1e-6.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
FaceSelection: All faces that lie in the specified plane
|
|
558
|
+
"""
|
|
559
|
+
orig = np.array([x,y,z])
|
|
560
|
+
norm = np.array([nx,ny,nz])
|
|
561
|
+
norm = norm/np.linalg.norm(norm)
|
|
562
|
+
|
|
563
|
+
dimtags = gmsh.model.getEntities(2)
|
|
564
|
+
coords = [gmsh.model.occ.getCenterOfMass(*tag) for tag in dimtags]
|
|
565
|
+
normals = [gmsh.model.get_normal(t, (0,0)) for d,t, in dimtags]
|
|
566
|
+
tags = []
|
|
567
|
+
for (d,t), o, n in zip(dimtags, coords, normals):
|
|
568
|
+
normdist = np.abs((o-orig)@norm)
|
|
569
|
+
dotnorm = np.abs(n@norm)
|
|
570
|
+
if normdist < tolerance and dotnorm > 1-tolerance:
|
|
571
|
+
tags.append(t)
|
|
572
|
+
return FaceSelection(tags)
|
|
573
|
+
|
|
574
|
+
def code(self, code: str):
|
|
575
|
+
nums1 = decode_data(code)
|
|
576
|
+
|
|
577
|
+
dimtags = gmsh.model.getEntities(2)
|
|
578
|
+
|
|
579
|
+
scoring = dict()
|
|
580
|
+
for dim, tag in dimtags:
|
|
581
|
+
x1, y1, z1 = gmsh.model.occ.getCenterOfMass(2, tag)
|
|
582
|
+
xmin2, ymin2, zmin2, xmax2, ymax2, zmax2 = gmsh.model.occ.getBoundingBox(dim, tag)
|
|
583
|
+
nums2 = [x1, y1, z1, xmin2, ymin2, zmin2, xmax2, ymax2, zmax2]
|
|
584
|
+
score = np.sqrt(sum([(a-b)**2 for a,b in zip(nums1, nums2)]))
|
|
585
|
+
scoring[tag] = score
|
|
586
|
+
|
|
587
|
+
min_val = min(scoring.values())
|
|
588
|
+
|
|
589
|
+
# Find all keys whose value == min_val
|
|
590
|
+
candidates = [k for k, v in scoring.items() if v == min_val]
|
|
591
|
+
|
|
592
|
+
# Pick the lowest key
|
|
593
|
+
lowest_key = min(candidates)
|
|
594
|
+
return FaceSelection([lowest_key,])
|
|
595
|
+
|
|
596
|
+
SELECTOR_OBJ = Selector()
|