emerge 0.6.0__py3-none-any.whl → 0.6.2__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 +1 -1
- emerge/_emerge/cs.py +15 -6
- emerge/_emerge/geo/__init__.py +2 -2
- emerge/_emerge/geo/pcb.py +2 -2
- emerge/_emerge/geo/pipes.py +62 -0
- emerge/_emerge/geo/polybased.py +298 -59
- emerge/_emerge/geo/shapes.py +25 -0
- emerge/_emerge/geometry.py +4 -0
- emerge/_emerge/mesh3d.py +5 -4
- emerge/_emerge/mth/optimized.py +0 -31
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +3 -8
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +3 -8
- emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +3 -8
- emerge/_emerge/physics/microwave/microwave_data.py +76 -24
- emerge/_emerge/physics/microwave/sc.py +10 -15
- emerge/_emerge/plot/simple_plots.py +0 -2
- emerge/_emerge/simmodel.py +1 -1
- emerge/lib.py +161 -160
- {emerge-0.6.0.dist-info → emerge-0.6.2.dist-info}/METADATA +2 -3
- {emerge-0.6.0.dist-info → emerge-0.6.2.dist-info}/RECORD +23 -22
- {emerge-0.6.0.dist-info → emerge-0.6.2.dist-info}/WHEEL +0 -0
- {emerge-0.6.0.dist-info → emerge-0.6.2.dist-info}/entry_points.txt +0 -0
- {emerge-0.6.0.dist-info → emerge-0.6.2.dist-info}/licenses/LICENSE +0 -0
emerge/__init__.py
CHANGED
emerge/_emerge/cs.py
CHANGED
|
@@ -101,7 +101,7 @@ class Axis:
|
|
|
101
101
|
"""
|
|
102
102
|
return self.pair(other)
|
|
103
103
|
|
|
104
|
-
def construct_cs(self) -> CoordinateSystem:
|
|
104
|
+
def construct_cs(self, origin: tuple[float, float, float] = (0.,0.,0.)) -> CoordinateSystem:
|
|
105
105
|
"""Constructs a coordinate system where this vector is the Z-axis
|
|
106
106
|
and the X and Y axis are normal to this axis but with an arbitrary rotation.
|
|
107
107
|
|
|
@@ -114,7 +114,7 @@ class Axis:
|
|
|
114
114
|
ax = Axis(np.array([0, 1, 0]))
|
|
115
115
|
ax1 = self.cross(ax)
|
|
116
116
|
ax2 = self.cross(ax1).neg
|
|
117
|
-
return CoordinateSystem(ax2, ax1, self, np.
|
|
117
|
+
return CoordinateSystem(ax2, ax1, self, np.array(origin))
|
|
118
118
|
|
|
119
119
|
XAX: Axis = Axis(np.array([1, 0, 0]))
|
|
120
120
|
YAX: Axis = Axis(np.array([0, 1, 0]))
|
|
@@ -318,7 +318,8 @@ class CoordinateSystem:
|
|
|
318
318
|
|
|
319
319
|
def rotate(self, axis: tuple | list | np.ndarray | Axis,
|
|
320
320
|
angle: float,
|
|
321
|
-
degrees: bool = True
|
|
321
|
+
degrees: bool = True,
|
|
322
|
+
origin: bool | np.ndarray = False) -> CoordinateSystem:
|
|
322
323
|
"""Return a new CoordinateSystem rotated about the given axis (through the global origin)
|
|
323
324
|
by `angle`. If `degrees` is True, `angle` is interpreted in degrees.
|
|
324
325
|
|
|
@@ -326,6 +327,7 @@ class CoordinateSystem:
|
|
|
326
327
|
axis (tuple | list | np.ndarray | Axis): The rotation axis
|
|
327
328
|
angle (float): The rotation angle (in degrees if degrees = True)
|
|
328
329
|
degrees (bool, optional): Whether to use degrees. Defaults to True.
|
|
330
|
+
origin (bool, np.array, optional): Whether to rotate the origin as well. Defaults to False.
|
|
329
331
|
|
|
330
332
|
Returns:
|
|
331
333
|
CoordinateSystem: The new rotated coordinate system
|
|
@@ -354,14 +356,21 @@ class CoordinateSystem:
|
|
|
354
356
|
new_x = R @ self.xax.vector
|
|
355
357
|
new_y = R @ self.yax.vector
|
|
356
358
|
new_z = R @ self.zax.vector
|
|
357
|
-
|
|
359
|
+
|
|
360
|
+
if origin is not False:
|
|
361
|
+
if isinstance(origin, bool):
|
|
362
|
+
new_o = R @ self.origin
|
|
363
|
+
else:
|
|
364
|
+
new_o = (R @ (self.origin-np.array(origin))) + np.array(origin)
|
|
365
|
+
else:
|
|
366
|
+
new_o = self.origin.copy()
|
|
358
367
|
|
|
359
368
|
return CoordinateSystem(
|
|
360
369
|
xax=new_x,
|
|
361
370
|
yax=new_y,
|
|
362
371
|
zax=new_z,
|
|
363
|
-
origin=
|
|
364
|
-
_is_global=
|
|
372
|
+
origin=new_o,
|
|
373
|
+
_is_global=False
|
|
365
374
|
)
|
|
366
375
|
|
|
367
376
|
def swapxy(self) -> None:
|
emerge/_emerge/geo/__init__.py
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
from .pcb import PCB
|
|
19
19
|
from .pmlbox import pmlbox
|
|
20
20
|
from .horn import Horn
|
|
21
|
-
from .shapes import Cylinder, CoaxCylinder, Box, XYPlate, HalfSphere, Sphere, Plate, OldBox, Alignment
|
|
21
|
+
from .shapes import Cylinder, CoaxCylinder, Box, XYPlate, HalfSphere, Sphere, Plate, OldBox, Alignment, Cone
|
|
22
22
|
from .operations import subtract, add, embed, remove, rotate, mirror, change_coordinate_system, translate, intersect
|
|
23
|
-
from .polybased import XYPolygon, GeoPrism
|
|
23
|
+
from .polybased import XYPolygon, GeoPrism, Disc, Curve
|
|
24
24
|
from .step import STEPItems
|
emerge/_emerge/geo/pcb.py
CHANGED
|
@@ -1079,7 +1079,7 @@ class PCB:
|
|
|
1079
1079
|
plane = change_coordinate_system(plane, self.cs) # type: ignore
|
|
1080
1080
|
return plane # type: ignore
|
|
1081
1081
|
|
|
1082
|
-
def
|
|
1082
|
+
def generate_pcb(self,
|
|
1083
1083
|
split_z: bool = True,
|
|
1084
1084
|
layer_tolerance: float = 1e-6,
|
|
1085
1085
|
merge: bool = True) -> GeoVolume:
|
|
@@ -1119,7 +1119,7 @@ class PCB:
|
|
|
1119
1119
|
box = change_coordinate_system(box, self.cs)
|
|
1120
1120
|
return box # type: ignore
|
|
1121
1121
|
|
|
1122
|
-
def
|
|
1122
|
+
def generate_air(self, height: float) -> GeoVolume:
|
|
1123
1123
|
"""Generate the Air Block object
|
|
1124
1124
|
|
|
1125
1125
|
This requires that the width, depth and origin are deterimed. This
|
|
@@ -0,0 +1,62 @@
|
|
|
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 typing import Literal, Callable
|
|
22
|
+
from ..geometry import GeoEdge, GeoSurface, GeoVolume
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Curve(GeoEdge):
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def __init__(self, xpts: np.ndarray, ypts: np.ndarray, zpts: np.ndarray,
|
|
30
|
+
degree: int = 3,
|
|
31
|
+
weights: list[float] | None = None,
|
|
32
|
+
knots: list[float] | None = None,
|
|
33
|
+
ctype: Literal['Spline','BSpline','Bezier'] = 'Bezier'):
|
|
34
|
+
self.xpts: np.ndarray = xpts
|
|
35
|
+
self.ypts: np.ndarray = ypts
|
|
36
|
+
self.zpts: np.ndarray = zpts
|
|
37
|
+
|
|
38
|
+
points = [gmsh.model.occ.add_point(x,y,z) for x,y,z in zip(xpts, ypts, zpts)]
|
|
39
|
+
|
|
40
|
+
if ctype.lower()=='spline':
|
|
41
|
+
tags = gmsh.model.occ.addSpline(points)
|
|
42
|
+
|
|
43
|
+
elif ctype.lower()=='bspline':
|
|
44
|
+
if weights is None:
|
|
45
|
+
weights = []
|
|
46
|
+
if knots is None:
|
|
47
|
+
knots = []
|
|
48
|
+
tags = gmsh.model.occ.addBSpline(points, degree=degree, weights=weights, knots=knots)
|
|
49
|
+
else:
|
|
50
|
+
tags = gmsh.model.occ.addBezier(points)
|
|
51
|
+
|
|
52
|
+
tags = gmsh.model.occ.addWire([tags,])
|
|
53
|
+
gmsh.model.occ.remove([(0,tag) for tag in points])
|
|
54
|
+
super().__init__(tags)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Helix(GeoVolume):
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
emerge/_emerge/geo/polybased.py
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
# <https://www.gnu.org/licenses/>.
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
import numpy as np
|
|
19
|
-
from ..cs import CoordinateSystem, GCS
|
|
20
|
-
from ..geometry import GeoVolume, GeoPolygon
|
|
19
|
+
from ..cs import CoordinateSystem, GCS, Axis
|
|
20
|
+
from ..geometry import GeoVolume, GeoPolygon, GeoEdge, GeoSurface
|
|
21
21
|
from .shapes import Alignment
|
|
22
22
|
import gmsh
|
|
23
23
|
from typing import Generator, Callable
|
|
@@ -158,6 +158,26 @@ def rotate_point(point: tuple[float, float, float],
|
|
|
158
158
|
rotated += o
|
|
159
159
|
return tuple(rotated)
|
|
160
160
|
|
|
161
|
+
|
|
162
|
+
def orthonormalize(axis: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
163
|
+
"""Generates a set of orthonormal vectors given an input vector X
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
axis (np.ndarray): The X-axis
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
tuple[np.ndarray, np.ndarray, np.ndarray]: The X, Y and Z axis (orthonormal)
|
|
170
|
+
"""
|
|
171
|
+
Xaxis = axis/np.linalg.norm(axis)
|
|
172
|
+
V = np.array([0,1,0])
|
|
173
|
+
if 1-np.abs(np.dot(Xaxis, V)) < 1e-12:
|
|
174
|
+
V = np.array([0,0,1])
|
|
175
|
+
Yaxis = np.cross(Xaxis, V)
|
|
176
|
+
Yaxis = np.abs(Yaxis/np.linalg.norm(Yaxis))
|
|
177
|
+
Zaxis = np.cross(Xaxis, Yaxis)
|
|
178
|
+
Zaxis = np.abs(Zaxis/np.linalg.norm(Zaxis))
|
|
179
|
+
return Xaxis, Yaxis, Zaxis
|
|
180
|
+
|
|
161
181
|
class GeoPrism(GeoVolume):
|
|
162
182
|
"""The GepPrism class generalizes the GeoVolume for extruded convex polygons.
|
|
163
183
|
Besides having a volumetric definitions, the class offers a .front_face
|
|
@@ -231,6 +251,10 @@ class XYPolygon:
|
|
|
231
251
|
|
|
232
252
|
self.fillets: list[tuple[float, int]] = []
|
|
233
253
|
|
|
254
|
+
@property
|
|
255
|
+
def length(self):
|
|
256
|
+
return sum([((self.x[i2]-self.x[i1])**2 + (self.y[i2]-self.y[i1])**2)**0.5 for i1, i2 in zip(range(self.N-1),range(1, self.N))])
|
|
257
|
+
|
|
234
258
|
@property
|
|
235
259
|
def N(self) -> int:
|
|
236
260
|
"""The number of polygon points
|
|
@@ -238,7 +262,7 @@ class XYPolygon:
|
|
|
238
262
|
Returns:
|
|
239
263
|
int: The number of points
|
|
240
264
|
"""
|
|
241
|
-
return len(self.
|
|
265
|
+
return len(self.x)
|
|
242
266
|
|
|
243
267
|
def _check(self) -> None:
|
|
244
268
|
"""Checks if the last point is the same as the first point.
|
|
@@ -329,7 +353,7 @@ class XYPolygon:
|
|
|
329
353
|
poly.lines = lines
|
|
330
354
|
return poly
|
|
331
355
|
|
|
332
|
-
def extrude(self, length: float, cs: CoordinateSystem = None) -> GeoPrism:
|
|
356
|
+
def extrude(self, length: float, cs: CoordinateSystem | None = None) -> GeoPrism:
|
|
333
357
|
"""Extrues the polygon along the Z-axis.
|
|
334
358
|
The z-coordinates go from z1 to z2 (in meters). Then the extrusion
|
|
335
359
|
is either provided by a maximum dz distance (in meters) or a number
|
|
@@ -351,7 +375,7 @@ class XYPolygon:
|
|
|
351
375
|
surftags = [t for d,t in volume if d==2]
|
|
352
376
|
return GeoPrism(tags, surftags[0], surftags)
|
|
353
377
|
|
|
354
|
-
def geo(self, cs: CoordinateSystem = None) -> GeoPolygon:
|
|
378
|
+
def geo(self, cs: CoordinateSystem | None = None) -> GeoPolygon:
|
|
355
379
|
"""Returns a GeoPolygon object for the current polygon.
|
|
356
380
|
|
|
357
381
|
Args:
|
|
@@ -388,9 +412,9 @@ class XYPolygon:
|
|
|
388
412
|
|
|
389
413
|
@staticmethod
|
|
390
414
|
def circle(radius: float,
|
|
391
|
-
dsmax: float = None,
|
|
392
|
-
tolerance: float = None,
|
|
393
|
-
Nsections: int = None):
|
|
415
|
+
dsmax: float | None= None,
|
|
416
|
+
tolerance: float | None = None,
|
|
417
|
+
Nsections: int | None = None):
|
|
394
418
|
"""This method generates a segmented circle.
|
|
395
419
|
|
|
396
420
|
The number of points along the circumpherence can be specified in 3 ways. By a maximum
|
|
@@ -476,54 +500,269 @@ class XYPolygon:
|
|
|
476
500
|
self.extend(xs, ys)
|
|
477
501
|
return self
|
|
478
502
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
503
|
+
|
|
504
|
+
class Disc(GeoSurface):
|
|
505
|
+
|
|
506
|
+
def __init__(self, origin: tuple[float, float, float],
|
|
507
|
+
radius: float,
|
|
508
|
+
axis: tuple[float, float, float] = (0,0,1.0)):
|
|
509
|
+
"""Creates a circular Disc surface.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
origin (tuple[float, float, float]): The center of the disc
|
|
513
|
+
radius (float): The radius of the disc
|
|
514
|
+
axis (tuple[float, float, float], optional): The disc normal axis. Defaults to (0,0,1.0).
|
|
515
|
+
"""
|
|
516
|
+
disc = gmsh.model.occ.addDisk(*origin, radius, radius, zAxis=axis)
|
|
517
|
+
super().__init__(disc)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class Curve(GeoEdge):
|
|
521
|
+
def __init__(self,
|
|
522
|
+
xpts: np.ndarray,
|
|
523
|
+
ypts: np.ndarray,
|
|
524
|
+
zpts: np.ndarray,
|
|
525
|
+
degree: int = 3,
|
|
526
|
+
weights: list[float] | None = None,
|
|
527
|
+
knots: list[float] | None = None,
|
|
528
|
+
ctype: Literal['Spline','BSpline','Bezier'] = 'Bezier',
|
|
529
|
+
dstart: tuple[float, float, float] | None = None):
|
|
530
|
+
"""Generate a Spline/Bspline or Bezier curve based on a series of points
|
|
531
|
+
|
|
532
|
+
This calls the different curve features in OpenCASCADE.
|
|
533
|
+
|
|
534
|
+
The dstart parameter defines the departure direction of the curve. If not provided this is inferred as the
|
|
535
|
+
discrete derivative from the first to second coordinate.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
xpts (np.ndarray): The X-coordinates
|
|
539
|
+
ypts (np.ndarray): The Y-coordinates
|
|
540
|
+
zpts (np.ndarray): The Z-coordinates
|
|
541
|
+
degree (int, optional): The BSpline degree parameter. Defaults to 3.
|
|
542
|
+
weights (list[float] | None, optional): An optional point weights list. Defaults to None.
|
|
543
|
+
knots (list[float] | None, optional): A nkots list. Defaults to None.
|
|
544
|
+
ctype (Literal['Spline','BSpline','Bezier'], optional): The type of curve. Defaults to 'Spline'.
|
|
545
|
+
dstart (tuple[float, float, float] | None, optional): The departure direction. Defaults to None.
|
|
546
|
+
"""
|
|
547
|
+
self.xpts: np.ndarray = xpts
|
|
548
|
+
self.ypts: np.ndarray = ypts
|
|
549
|
+
self.zpts: np.ndarray = zpts
|
|
550
|
+
|
|
551
|
+
if dstart is None:
|
|
552
|
+
dstart = (xpts[1]-xpts[0], ypts[1]-ypts[0], zpts[1]-zpts[0])
|
|
553
|
+
|
|
554
|
+
self.dstart: tuple[float, float, float] = dstart
|
|
555
|
+
|
|
556
|
+
points = [gmsh.model.occ.add_point(x,y,z) for x,y,z in zip(xpts, ypts, zpts)]
|
|
557
|
+
|
|
558
|
+
if ctype.lower()=='spline':
|
|
559
|
+
tags = gmsh.model.occ.addSpline(points)
|
|
560
|
+
|
|
561
|
+
elif ctype.lower()=='bspline':
|
|
562
|
+
if weights is None:
|
|
563
|
+
weights = []
|
|
564
|
+
if knots is None:
|
|
565
|
+
knots = []
|
|
566
|
+
tags = gmsh.model.occ.addBSpline(points, degree=degree, weights=weights, knots=knots)
|
|
567
|
+
else:
|
|
568
|
+
tags = gmsh.model.occ.addBezier(points)
|
|
569
|
+
|
|
570
|
+
tags = gmsh.model.occ.addWire([tags,])
|
|
571
|
+
gmsh.model.occ.remove([(0,tag) for tag in points])
|
|
572
|
+
super().__init__(tags)
|
|
573
|
+
|
|
574
|
+
@property
|
|
575
|
+
def p0(self) -> tuple[float, float, float]:
|
|
576
|
+
"""The start coordinate
|
|
577
|
+
"""
|
|
578
|
+
return (self.xpts[0], self.ypts[0], self.zpts[0])
|
|
579
|
+
|
|
580
|
+
@staticmethod
|
|
581
|
+
def helix_rh(pstart: tuple[float, float, float],
|
|
582
|
+
pend: tuple[float, float, float],
|
|
583
|
+
r_start: float,
|
|
584
|
+
pitch: float,
|
|
585
|
+
r_end: float | None = None,
|
|
586
|
+
_narc: int = 8,
|
|
587
|
+
startfeed: float = 0.0) -> Curve:
|
|
588
|
+
"""Generates a Helical curve
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
pstart (tuple[float, float, float]): The start of the center of rotation (not the start of the curve)
|
|
592
|
+
pend (tuple[float, float, float]): The end of the center of rotation
|
|
593
|
+
r_start (float): The (start) radius of the helix
|
|
594
|
+
pitch (float): The pitch angle of the helix
|
|
595
|
+
r_end (float | None, optional): The ending radius. If default, the same is used as the start. Defaults to None.
|
|
596
|
+
_narc (int, optional): The number of Spline arc sections used. Defaults to 8.
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
Curve: The Curve geometry object
|
|
600
|
+
"""
|
|
601
|
+
if r_end is None:
|
|
602
|
+
r_end = r_start
|
|
603
|
+
|
|
604
|
+
pitch = pitch*np.pi/180
|
|
605
|
+
|
|
606
|
+
R1, R2, DR = r_start, r_end, r_end-r_start
|
|
607
|
+
p0 = np.array(pstart)
|
|
608
|
+
p1 = np.array(pend)
|
|
609
|
+
dp = (p1-p0)
|
|
610
|
+
L = (dp[0]**2 + dp[1]**2 + dp[2]**2)**(0.5)
|
|
611
|
+
dp = dp/L
|
|
612
|
+
|
|
613
|
+
Z, X, Y = orthonormalize(dp)
|
|
614
|
+
|
|
615
|
+
Q = L/np.tan(pitch)
|
|
616
|
+
#a1 = Q/R1
|
|
617
|
+
#a2 = Q*((1 - 1/R1 *(R2 + DR))/(2*R2 + DR))
|
|
618
|
+
C = 0#Q/R1
|
|
619
|
+
|
|
620
|
+
wtot = C/R2 + Q/R2
|
|
621
|
+
nt = int(np.ceil(wtot/(2*np.pi)*_narc))
|
|
622
|
+
|
|
623
|
+
t = np.linspace(0, 1, nt)
|
|
624
|
+
Rt = R1 + DR*t
|
|
625
|
+
#wt = (a1*t + a2*t**2)
|
|
626
|
+
wt = C/Rt + (Q*t)/Rt
|
|
627
|
+
|
|
628
|
+
xs = (R1 + DR*t)*np.cos(wt)
|
|
629
|
+
ys = (R1 + DR*t)*np.sin(wt)
|
|
630
|
+
zs = L*t
|
|
631
|
+
|
|
632
|
+
xp = xs*X[0] + ys*Y[0] + zs*Z[0] + p0[0]
|
|
633
|
+
yp = xs*X[1] + ys*Y[1] + zs*Z[1] + p0[1]
|
|
634
|
+
zp = xs*X[2] + ys*Y[2] + zs*Z[2] + p0[2]
|
|
635
|
+
|
|
636
|
+
dp = tuple(Y)
|
|
637
|
+
if startfeed > 0:
|
|
638
|
+
dpx, dpy, dpz = Y
|
|
639
|
+
dx = Z[0]
|
|
640
|
+
dy = Z[1]
|
|
641
|
+
dz = Z[2]
|
|
642
|
+
d = startfeed
|
|
643
|
+
|
|
644
|
+
fx = np.array([xp[0] - dx*d/2 - d*dpx, xp[0] - dx*d*0.8/2 - d*dpx])
|
|
645
|
+
fy = np.array([yp[0] - dy*d/2 - d*dpy, yp[0] - dy*d*0.8/2 - d*dpy])
|
|
646
|
+
fz = np.array([zp[0] - dz*d/2 - d*dpz, zp[0] - dz*d*0.8/2 - d*dpz])
|
|
647
|
+
|
|
648
|
+
xp = np.concat([fx ,xp])
|
|
649
|
+
yp = np.concat([fy, yp])
|
|
650
|
+
zp = np.concat([fz, zp])
|
|
651
|
+
xp[2] += d/2*dx
|
|
652
|
+
yp[2] += d/2*dy
|
|
653
|
+
zp[2] += d/2*dz
|
|
654
|
+
dp = tuple(Z)
|
|
655
|
+
|
|
656
|
+
return Curve(xp, yp, zp, ctype='Spline', dstart=dp)
|
|
657
|
+
|
|
658
|
+
@staticmethod
|
|
659
|
+
def helix_lh(pstart: tuple[float, float, float],
|
|
660
|
+
pend: tuple[float, float, float],
|
|
661
|
+
r_start: float,
|
|
662
|
+
pitch: float,
|
|
663
|
+
r_end: float | None = None,
|
|
664
|
+
_narc: int = 8,
|
|
665
|
+
startfeed: float = 0.0) -> Curve:
|
|
666
|
+
"""Generates a Helical curve
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
pstart (tuple[float, float, float]): The start of the center of rotation (not the start of the curve)
|
|
670
|
+
pend (tuple[float, float, float]): The end of the center of rotation
|
|
671
|
+
r_start (float): The (start) radius of the helix
|
|
672
|
+
pitch (float): The pitch angle of the helix
|
|
673
|
+
r_end (float | None, optional): The ending radius. If default, the same is used as the start. Defaults to None.
|
|
674
|
+
_narc (int, optional): The number of Spline arc sections used. Defaults to 8.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
Curve: The Curve geometry object
|
|
678
|
+
"""
|
|
679
|
+
if r_end is None:
|
|
680
|
+
r_end = r_start
|
|
681
|
+
|
|
682
|
+
pitch = pitch*np.pi/180
|
|
683
|
+
|
|
684
|
+
R1, R2, DR = r_start, r_end, r_end-r_start
|
|
685
|
+
p0 = np.array(pstart)
|
|
686
|
+
p1 = np.array(pend)
|
|
687
|
+
dp = (p1-p0)
|
|
688
|
+
L = (dp[0]**2 + dp[1]**2 + dp[2]**2)**(0.5)
|
|
689
|
+
dp = dp/L
|
|
690
|
+
|
|
691
|
+
Z, X, Y = orthonormalize(dp)
|
|
692
|
+
|
|
693
|
+
Q = L/np.tan(pitch)
|
|
694
|
+
#a1 = Q/R1
|
|
695
|
+
#a2 = Q*((1 - 1/R1 *(R2 + DR))/(2*R2 + DR))
|
|
696
|
+
C = 0#Q/R1
|
|
697
|
+
|
|
698
|
+
wtot = C/R2 + Q/R2
|
|
699
|
+
nt = int(np.ceil(wtot/(2*np.pi)*_narc))
|
|
700
|
+
|
|
701
|
+
t = np.linspace(0, 1, nt)
|
|
702
|
+
Rt = R1 + DR*t
|
|
703
|
+
#wt = (a1*t + a2*t**2)
|
|
704
|
+
wt = C/Rt + (Q*t)/Rt
|
|
705
|
+
|
|
706
|
+
xs = (R1 + DR*t)*np.cos(-wt)
|
|
707
|
+
ys = (R1 + DR*t)*np.sin(-wt)
|
|
708
|
+
zs = L*t
|
|
709
|
+
|
|
710
|
+
xp = xs*X[0] + ys*Y[0] + zs*Z[0] + p0[0]
|
|
711
|
+
yp = xs*X[1] + ys*Y[1] + zs*Z[1] + p0[1]
|
|
712
|
+
zp = xs*X[2] + ys*Y[2] + zs*Z[2] + p0[2]
|
|
713
|
+
|
|
714
|
+
dp = tuple(Y)
|
|
715
|
+
if startfeed > 0:
|
|
716
|
+
dpx, dpy, dpz = Y
|
|
717
|
+
dx = Z[0]
|
|
718
|
+
dy = Z[1]
|
|
719
|
+
dz = Z[2]
|
|
720
|
+
d = startfeed
|
|
721
|
+
|
|
722
|
+
fx = np.array([xp[0] - dx*d/2 + d*dpx, xp[0] - dx*d*0.8/2 + d*dpx])
|
|
723
|
+
fy = np.array([yp[0] - dy*d/2 + d*dpy, yp[0] - dy*d*0.8/2 + d*dpy])
|
|
724
|
+
fz = np.array([zp[0] - dz*d/2 + d*dpz, zp[0] - dz*d*0.8/2 + d*dpz])
|
|
725
|
+
|
|
726
|
+
xp = np.concat([fx ,xp])
|
|
727
|
+
yp = np.concat([fy, yp])
|
|
728
|
+
zp = np.concat([fz, zp])
|
|
729
|
+
xp[2] += d/2*dx
|
|
730
|
+
yp[2] += d/2*dy
|
|
731
|
+
zp[2] += d/2*dz
|
|
732
|
+
dp = tuple(Z)
|
|
733
|
+
|
|
734
|
+
return Curve(xp, yp, zp, ctype='Spline', dstart=dp)
|
|
735
|
+
|
|
736
|
+
def pipe(self, crossection: GeoSurface | XYPolygon, max_mesh_size: float | None = None) -> GeoVolume:
|
|
737
|
+
"""Extrudes a surface or XYPolygon object along the given curve
|
|
738
|
+
|
|
739
|
+
If a GeoSurface object is used, make sure it starts at the center of the curve. This property
|
|
740
|
+
can be accessed with curve_obj.p0.Alignment
|
|
741
|
+
If an XYPolygon is used, it will be automatically centered with XY=0 at the start of the curve with
|
|
742
|
+
the Z-axis align along the initial departure direction curve_obj.dstart.Alignment
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
crossection (GeoSurface | XYPolygon): The cross section definition to be used
|
|
746
|
+
max_mesh_size (float, optional): The maximum mesh size. Defaults to None
|
|
747
|
+
Returns:
|
|
748
|
+
GeoVolume: The resultant volume object
|
|
749
|
+
"""
|
|
750
|
+
if isinstance(crossection, XYPolygon):
|
|
751
|
+
zax = self.dstart
|
|
752
|
+
cs = Axis(np.array(zax)).construct_cs(self.p0)
|
|
753
|
+
surf = crossection.geo(cs)
|
|
754
|
+
else:
|
|
755
|
+
surf = crossection
|
|
756
|
+
x1, y1, z1, x2, y2, z2 = gmsh.model.occ.getBoundingBox(*surf.dimtags[0])
|
|
757
|
+
diag = ((x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2)**(0.5)
|
|
758
|
+
|
|
759
|
+
pipetag = gmsh.model.occ.addPipe(surf.dimtags, self.tags[0], 'GuidePlan')
|
|
760
|
+
|
|
761
|
+
self.remove()
|
|
762
|
+
surf.remove()
|
|
763
|
+
|
|
764
|
+
volume = GeoVolume(pipetag[0][1])
|
|
765
|
+
|
|
766
|
+
volume.max_meshsize = diag/2
|
|
767
|
+
return volume
|
|
768
|
+
|
emerge/_emerge/geo/shapes.py
CHANGED
|
@@ -448,3 +448,28 @@ class OldBox(GeoVolume):
|
|
|
448
448
|
|
|
449
449
|
tags = list(reduce(lambda a,b: a+b, tagslist))
|
|
450
450
|
return FaceSelection(tags)
|
|
451
|
+
|
|
452
|
+
class Cone(GeoVolume):
|
|
453
|
+
|
|
454
|
+
def __init__(self, p0: tuple[float, float, float],
|
|
455
|
+
direction: tuple[float, float, float],
|
|
456
|
+
r1: float,
|
|
457
|
+
r2: float):
|
|
458
|
+
"""Constructis a cone that starts at position p0 and is aimed in the given direction.
|
|
459
|
+
r1 is the start radius and r2 the end radius. The magnitude of direction determines its length.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
p0 (tuple[float, float, float]): _description_
|
|
463
|
+
direction (tuple[float, float, float]): _description_
|
|
464
|
+
r1 (float): _description_
|
|
465
|
+
r2 (float): _description_
|
|
466
|
+
"""
|
|
467
|
+
tag = gmsh.model.occ.add_cone(*p0, *direction, r1, r2)
|
|
468
|
+
super().__init__(tag)
|
|
469
|
+
|
|
470
|
+
p0 = np.array(p0)
|
|
471
|
+
ds = np.array(direction)
|
|
472
|
+
|
|
473
|
+
self._add_face_pointer('front', p0, ds)
|
|
474
|
+
if r2>0:
|
|
475
|
+
self._add_face_pointer('back', p0+ds, ds)
|
emerge/_emerge/geometry.py
CHANGED
|
@@ -472,6 +472,10 @@ class GeoObject:
|
|
|
472
472
|
return GeoVolume(tags)
|
|
473
473
|
return GeoObject(tags)
|
|
474
474
|
|
|
475
|
+
def remove(self) -> None:
|
|
476
|
+
self._exists = False
|
|
477
|
+
gmsh.model.occ.remove(self.dimtags, True)
|
|
478
|
+
|
|
475
479
|
class GeoVolume(GeoObject):
|
|
476
480
|
'''GeoVolume is an interface to the GMSH CAD kernel. It does not represent EMerge
|
|
477
481
|
specific geometry data.'''
|
emerge/_emerge/mesh3d.py
CHANGED
|
@@ -170,13 +170,13 @@ class Mesh3D(Mesh):
|
|
|
170
170
|
'''Return the number of nodes'''
|
|
171
171
|
return self.nodes.shape[1]
|
|
172
172
|
|
|
173
|
-
def get_edge(self, i1: int, i2: int) -> int:
|
|
173
|
+
def get_edge(self, i1: int, i2: int, skip: bool = False) -> int:
|
|
174
174
|
'''Return the edge index given the two node indices'''
|
|
175
175
|
if i1==i2:
|
|
176
176
|
raise ValueError("Edge cannot be formed by the same node.")
|
|
177
177
|
search = (min(int(i1),int(i2)), max(int(i1),int(i2)))
|
|
178
178
|
result = self.inv_edges.get(search, -10)
|
|
179
|
-
if result == -10:
|
|
179
|
+
if result == -10 and not skip:
|
|
180
180
|
raise ValueError(f'There is no edge with indices {i1}, {i2}')
|
|
181
181
|
return result
|
|
182
182
|
|
|
@@ -454,7 +454,8 @@ class Mesh3D(Mesh):
|
|
|
454
454
|
edge_tags = np.array(edge_tags).flatten()
|
|
455
455
|
ent = np.array(edge_node_tags).reshape(-1,2).T
|
|
456
456
|
nET = ent.shape[1]
|
|
457
|
-
self.edge_t2i = {int(edge_tags[i]): self.get_edge(self.n_t2i[ent[0,i]], self.n_t2i[ent[1,i]]) for i in range(nET)}
|
|
457
|
+
self.edge_t2i = {int(edge_tags[i]): self.get_edge(self.n_t2i[ent[0,i]], self.n_t2i[ent[1,i]], skip=True) for i in range(nET)}
|
|
458
|
+
self.edge_t2i = {key: value for key,value in self.edge_t2i.items() if value!=-10}
|
|
458
459
|
self.edge_i2t = {i: t for t, i in self.edge_t2i.items()}
|
|
459
460
|
|
|
460
461
|
edge_dimtags = gmsh.model.get_entities(1)
|
|
@@ -463,7 +464,7 @@ class Mesh3D(Mesh):
|
|
|
463
464
|
if not edge_tags:
|
|
464
465
|
self.etag_to_edge[t] = []
|
|
465
466
|
continue
|
|
466
|
-
self.etag_to_edge[t] = [int(self.edge_t2i
|
|
467
|
+
self.etag_to_edge[t] = [int(self.edge_t2i.get(tag,None)) for tag in edge_tags[0] if tag in self.edge_t2i]
|
|
467
468
|
|
|
468
469
|
|
|
469
470
|
## Tag bindings
|