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.

Files changed (80) hide show
  1. emerge/__init__.py +54 -0
  2. emerge/__main__.py +5 -0
  3. emerge/_emerge/__init__.py +42 -0
  4. emerge/_emerge/bc.py +197 -0
  5. emerge/_emerge/coord.py +119 -0
  6. emerge/_emerge/cs.py +523 -0
  7. emerge/_emerge/dataset.py +36 -0
  8. emerge/_emerge/elements/__init__.py +19 -0
  9. emerge/_emerge/elements/femdata.py +212 -0
  10. emerge/_emerge/elements/index_interp.py +64 -0
  11. emerge/_emerge/elements/legrange2.py +172 -0
  12. emerge/_emerge/elements/ned2_interp.py +645 -0
  13. emerge/_emerge/elements/nedelec2.py +140 -0
  14. emerge/_emerge/elements/nedleg2.py +217 -0
  15. emerge/_emerge/geo/__init__.py +24 -0
  16. emerge/_emerge/geo/horn.py +107 -0
  17. emerge/_emerge/geo/modeler.py +449 -0
  18. emerge/_emerge/geo/operations.py +254 -0
  19. emerge/_emerge/geo/pcb.py +1244 -0
  20. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  21. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  22. emerge/_emerge/geo/pmlbox.py +204 -0
  23. emerge/_emerge/geo/polybased.py +529 -0
  24. emerge/_emerge/geo/shapes.py +427 -0
  25. emerge/_emerge/geo/step.py +77 -0
  26. emerge/_emerge/geo2d.py +86 -0
  27. emerge/_emerge/geometry.py +510 -0
  28. emerge/_emerge/howto.py +214 -0
  29. emerge/_emerge/logsettings.py +5 -0
  30. emerge/_emerge/material.py +118 -0
  31. emerge/_emerge/mesh3d.py +730 -0
  32. emerge/_emerge/mesher.py +339 -0
  33. emerge/_emerge/mth/common_functions.py +33 -0
  34. emerge/_emerge/mth/integrals.py +71 -0
  35. emerge/_emerge/mth/optimized.py +357 -0
  36. emerge/_emerge/periodic.py +263 -0
  37. emerge/_emerge/physics/__init__.py +0 -0
  38. emerge/_emerge/physics/microwave/__init__.py +1 -0
  39. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  40. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  41. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  42. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  43. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  44. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  45. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  46. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  47. emerge/_emerge/physics/microwave/periodic.py +82 -0
  48. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  49. emerge/_emerge/physics/microwave/sc.py +175 -0
  50. emerge/_emerge/physics/microwave/simjob.py +147 -0
  51. emerge/_emerge/physics/microwave/sparam.py +138 -0
  52. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  53. emerge/_emerge/plot/__init__.py +0 -0
  54. emerge/_emerge/plot/display.py +394 -0
  55. emerge/_emerge/plot/grapher.py +93 -0
  56. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  57. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  58. emerge/_emerge/plot/pyvista/display.py +931 -0
  59. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  60. emerge/_emerge/plot/simple_plots.py +551 -0
  61. emerge/_emerge/plot.py +225 -0
  62. emerge/_emerge/projects/__init__.py +0 -0
  63. emerge/_emerge/projects/_gen_base.txt +32 -0
  64. emerge/_emerge/projects/_load_base.txt +24 -0
  65. emerge/_emerge/projects/generate_project.py +40 -0
  66. emerge/_emerge/selection.py +596 -0
  67. emerge/_emerge/simmodel.py +444 -0
  68. emerge/_emerge/simulation_data.py +411 -0
  69. emerge/_emerge/solver.py +993 -0
  70. emerge/_emerge/system.py +54 -0
  71. emerge/cli.py +19 -0
  72. emerge/lib.py +57 -0
  73. emerge/plot.py +1 -0
  74. emerge/pyvista.py +1 -0
  75. {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
  76. emerge-0.4.8.dist-info/RECORD +78 -0
  77. emerge-0.4.8.dist-info/entry_points.txt +2 -0
  78. emerge-0.4.6.dist-info/RECORD +0 -4
  79. emerge-0.4.6.dist-info/entry_points.txt +0 -2
  80. {emerge-0.4.6.dist-info → emerge-0.4.8.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()