emerge 0.6.1__py3-none-any.whl → 0.6.3__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 CHANGED
@@ -18,7 +18,7 @@ along with this program; if not, see
18
18
  """
19
19
  import os
20
20
 
21
- __version__ = "0.6.1"
21
+ __version__ = "0.6.3"
22
22
 
23
23
  ############################################################
24
24
  # HANDLE ENVIRONMENT VARIABLES #
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.zeros(3))
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) -> CoordinateSystem:
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
- #new_o = R @ self.origin
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=self.origin,
364
- _is_global=self._is_global
372
+ origin=new_o,
373
+ _is_global=False
365
374
  )
366
375
 
367
376
  def swapxy(self) -> None:
@@ -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
@@ -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
+
@@ -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.xs)
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
- # def discrete_revolve(self, cs: CoordinateSystem, origin: tuple[float, float, float], axis: tuple[float, float,float], angle: float = 360.0, nsteps: int = 12) -> GeoPrism:
480
- # """Applies a revolution to the XYPolygon along the coordinate system Z-axis
481
-
482
- # Args:
483
- # cs (CoordinateSystem, optional): _description_. Defaults to None.
484
- # angle (float, optional): _description_. Defaults to 360.0.
485
-
486
- # Returns:
487
- # Prism: The resultant
488
- # """
489
- # if cs is None:
490
- # cs = GCS
491
-
492
- # x,y,z = origin
493
- # ax, ay, az = axis
494
- # loops = []
495
- # loops_edges = []
496
-
497
- # closed = False
498
- # if angle == 360:
499
- # angs = np.linspace(0, 2*np.pi, nsteps+1)[:-1]
500
- # closed = True
501
- # else:
502
- # angs = np.linspace(0, angle*np.pi/180, nsteps)
503
-
504
- # for x0, y0 in zip(self.x, self.y):
505
- # #print([rotate_point((x0, y0, 0), axis, ang, origin, degrees=False) for ang in angs])
506
- # points = [gmsh.model.occ.add_point(*rotate_point((x0, y0, 0), axis, ang, origin, degrees=False)) for ang in angs]
507
- # points = points + [points[0],]
508
- # loops.append(points)
509
-
510
- # edges = [gmsh.model.occ.add_line(p1, p2) for p1, p2 in zip(points[:-1],points[1:])]
511
- # loops_edges.append(edges)
512
-
513
- # face1loop = gmsh.model.occ.add_curve_loop(loops_edges[0])
514
- # face_front = gmsh.model.occ.add_plane_surface([face1loop,])
515
-
516
- # face2loop = gmsh.model.occ.add_curve_loop(loops_edges[-1])
517
- # face_back = gmsh.model.occ.add_plane_surface([face2loop,])
518
-
519
- # faces = []
520
- # for loop1, loop2 in zip(loops_edges[:-1], loops_edges[1:]):
521
- # for p1, p2, p3, p4 in zip(loop1[:-1], loop1[1:], loop2[1:], loop2[:0]):
522
- # curve = gmsh.model.occ.add_curve_loop([p1, p2, p3, p4])
523
- # face = gmsh.model.occ.add_plane_surface(curve)
524
- # faces.append(face)
525
-
526
- # surface_loop = gmsh.model.occ.add_surface_loop(faces + [face_front, face_back])
527
- # vol = gmsh.model.occ.add_volume([surface_loop,])
528
-
529
- # return GeoVolume(vol)
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
+
@@ -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)
@@ -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[tag]) for tag in edge_tags[0]]
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
@@ -358,37 +358,6 @@ def local_mapping(vertex_ids, triangle_ids):
358
358
 
359
359
  return out
360
360
 
361
- @njit(f8[:,:](f8[:], f8[:], f8[:]), cache=True, nogil=True)
362
- def orthonormal_basis(xs: np.ndarray, ys: np.ndarray, zs: np.ndarray):
363
- """
364
- Returns an orthonormal basis for the tetrahedron defined by the points
365
- xs, ys, zs. The basis is given as a 3x3 matrix with the first column being
366
- the normal vector of the face opposite to the first vertex.
367
- """
368
- x1, x2, x3 = xs
369
- y1, y2, y3 = ys
370
- z1, z2, z3 = zs
371
- e1x, e1y, e1z = x2-x1, y2-y1, z2-z1
372
- e2x, e2y, e2z = x3-x1, y3-y1, z3-z1
373
-
374
- nn = np.array([e2y*e1z - e2z*e1y,
375
- e2z*e1x - e2x*e1z,
376
- e2y*e1x - e2x*e1y])
377
-
378
- nn = nn/np.sqrt(nn[0]**2 + nn[1]**2 + nn[2]**2)
379
- n2 = np.array([e1x, e1y, e1z])/np.sqrt(e1x**2 + e1y**2 + e1z**2)
380
- n1 = np.array([n2[1]*nn[2] - n2[2]*nn[1],
381
- n2[2]*nn[0] - n2[0]*nn[2],
382
- n2[0]*nn[1] - n2[1]*nn[0]])
383
-
384
- if dot(n1, cross(n2, nn)) < 0:
385
- n1 = -n1
386
-
387
- B = np.zeros((3,3), dtype=np.float64)
388
- B[:,0] = n1
389
- B[:,1] = n2
390
- B[:,2] = nn
391
- return B
392
361
 
393
362
  @njit(f8[:,:](f8[:], f8[:], f8[:]), cache=True, nogil=True, fastmath=True)
394
363
  def compute_distances(xs: np.ndarray, ys: np.ndarray, zs: np.ndarray) -> np.ndarray: