emerge 0.4.7__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 (78) hide show
  1. emerge/__init__.py +14 -14
  2. emerge/_emerge/__init__.py +42 -0
  3. emerge/_emerge/bc.py +197 -0
  4. emerge/_emerge/coord.py +119 -0
  5. emerge/_emerge/cs.py +523 -0
  6. emerge/_emerge/dataset.py +36 -0
  7. emerge/_emerge/elements/__init__.py +19 -0
  8. emerge/_emerge/elements/femdata.py +212 -0
  9. emerge/_emerge/elements/index_interp.py +64 -0
  10. emerge/_emerge/elements/legrange2.py +172 -0
  11. emerge/_emerge/elements/ned2_interp.py +645 -0
  12. emerge/_emerge/elements/nedelec2.py +140 -0
  13. emerge/_emerge/elements/nedleg2.py +217 -0
  14. emerge/_emerge/geo/__init__.py +24 -0
  15. emerge/_emerge/geo/horn.py +107 -0
  16. emerge/_emerge/geo/modeler.py +449 -0
  17. emerge/_emerge/geo/operations.py +254 -0
  18. emerge/_emerge/geo/pcb.py +1244 -0
  19. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  20. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  21. emerge/_emerge/geo/pmlbox.py +204 -0
  22. emerge/_emerge/geo/polybased.py +529 -0
  23. emerge/_emerge/geo/shapes.py +427 -0
  24. emerge/_emerge/geo/step.py +77 -0
  25. emerge/_emerge/geo2d.py +86 -0
  26. emerge/_emerge/geometry.py +510 -0
  27. emerge/_emerge/howto.py +214 -0
  28. emerge/_emerge/logsettings.py +5 -0
  29. emerge/_emerge/material.py +118 -0
  30. emerge/_emerge/mesh3d.py +730 -0
  31. emerge/_emerge/mesher.py +339 -0
  32. emerge/_emerge/mth/common_functions.py +33 -0
  33. emerge/_emerge/mth/integrals.py +71 -0
  34. emerge/_emerge/mth/optimized.py +357 -0
  35. emerge/_emerge/periodic.py +263 -0
  36. emerge/_emerge/physics/__init__.py +0 -0
  37. emerge/_emerge/physics/microwave/__init__.py +1 -0
  38. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  39. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  40. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  41. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  42. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  43. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  44. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  45. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  46. emerge/_emerge/physics/microwave/periodic.py +82 -0
  47. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  48. emerge/_emerge/physics/microwave/sc.py +175 -0
  49. emerge/_emerge/physics/microwave/simjob.py +147 -0
  50. emerge/_emerge/physics/microwave/sparam.py +138 -0
  51. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  52. emerge/_emerge/plot/__init__.py +0 -0
  53. emerge/_emerge/plot/display.py +394 -0
  54. emerge/_emerge/plot/grapher.py +93 -0
  55. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  56. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  57. emerge/_emerge/plot/pyvista/display.py +931 -0
  58. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  59. emerge/_emerge/plot/simple_plots.py +551 -0
  60. emerge/_emerge/plot.py +225 -0
  61. emerge/_emerge/projects/__init__.py +0 -0
  62. emerge/_emerge/projects/_gen_base.txt +32 -0
  63. emerge/_emerge/projects/_load_base.txt +24 -0
  64. emerge/_emerge/projects/generate_project.py +40 -0
  65. emerge/_emerge/selection.py +596 -0
  66. emerge/_emerge/simmodel.py +444 -0
  67. emerge/_emerge/simulation_data.py +411 -0
  68. emerge/_emerge/solver.py +993 -0
  69. emerge/_emerge/system.py +54 -0
  70. emerge/cli.py +19 -0
  71. emerge/lib.py +1 -1
  72. emerge/plot.py +1 -1
  73. {emerge-0.4.7.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
  74. emerge-0.4.8.dist-info/RECORD +78 -0
  75. emerge-0.4.8.dist-info/entry_points.txt +2 -0
  76. emerge-0.4.7.dist-info/RECORD +0 -9
  77. emerge-0.4.7.dist-info/entry_points.txt +0 -2
  78. {emerge-0.4.7.dist-info → emerge-0.4.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,529 @@
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
+ from __future__ import annotations
18
+ import numpy as np
19
+ from ..cs import CoordinateSystem, GCS
20
+ from ..geometry import GeoVolume, GeoPolygon
21
+ from .shapes import Alignment
22
+ import gmsh
23
+ from typing import Generator, Callable
24
+ from ..selection import FaceSelection
25
+ from typing import Literal
26
+ from functools import reduce
27
+ from numba import njit
28
+
29
+ @njit(cache=True)
30
+ def _subsample_coordinates(xs: np.ndarray, ys: np.ndarray, tolerance: float, xmin: float) -> tuple[np.ndarray, np.ndarray]:
31
+ """This function takes a set of x and y coordinates in a finely sampled set and returns a reduced
32
+ set of numbers that traces the input curve within a provided tolerance.
33
+
34
+ Args:
35
+ xs (np.ndarray): The set of X-coordinates
36
+ ys (np.ndarray): The set of Y-coordinates
37
+ tolerance (float): The maximum deviation of the curve in meters
38
+ xmin (float): The minimal distance to the next point.
39
+
40
+ Returns:
41
+ np.ndarray: The output X-coordinates
42
+ np.ndarray: The output Y-coordinates
43
+ """
44
+ N = xs.shape[0]
45
+ ids = np.zeros((N,), dtype=np.int32)
46
+ store_index = 1
47
+ start_index = 0
48
+ final_index = 0
49
+ for iteration in range(N):
50
+ i1 = start_index
51
+ done = 0
52
+ for i2 in range(i1+1,N):
53
+ x_true = xs[i1:i2+1]
54
+ y_true = ys[i1:i2+1]
55
+
56
+ x_f = np.linspace(xs[i1],xs[i2], i2-i1+1)
57
+ y_f = np.linspace(ys[i1],ys[i2], i2-i1+1)
58
+ error = np.max(np.sqrt((x_f-x_true)**2 + (y_f-y_true)**2))
59
+ ds = np.sqrt((xs[i2]-xs[i1])**2 + (ys[i2]-ys[i1])**2)
60
+ # If at the end
61
+ if i2==N-1:
62
+ ids[store_index] = i2-1
63
+ final_index = store_index + 1
64
+ done = 1
65
+ break
66
+ # If not yet past the minimum distance, accumulate more
67
+ if ds < xmin:
68
+ continue
69
+ # If the end is less than a minimum distance
70
+ if np.sqrt((ys[-1]-ys[i2])**2 + (xs[-1]-xs[i2])**2) < xmin:
71
+ imid = i1 + (N-1-i1)//2
72
+ ids[store_index] = imid
73
+ ids[store_index+1] = N-1
74
+ final_index = store_index + 2
75
+ done = 1
76
+ break
77
+ if error < tolerance:
78
+ continue
79
+ else:
80
+ ids[store_index] = i2-1
81
+ start_index = i2
82
+ store_index = store_index + 1
83
+ break
84
+ if done==1:
85
+ break
86
+ return xs[ids[0:final_index]], ys[ids[0:final_index]]
87
+
88
+ def _discretize_curve(xfunc: Callable, yfunc: Callable,
89
+ t0: float, t1: float, xmin: float, tol: float=1e-4) -> tuple[np.ndarray, np.ndarray]:
90
+ """Computes a discreteized curve in X/Y coordinates based on the input parametric coordinates.
91
+
92
+ Args:
93
+ xfunc (Callable): The X-coordinate function fx(t)
94
+ yfunc (Callable): The Y-coordinate function fy(t)
95
+ t0 (float): The minimum value for the t-prameter
96
+ t1 (float): The maximum value for the t-parameter
97
+ xmin (float): The minimum distance for subsequent points
98
+ tol (float, optional): The curve matching tolerance. Defaults to 1e-4.
99
+
100
+ Returns:
101
+ tuple[np.ndarray, np.ndarray]: _description_
102
+ """
103
+ td = np.linspace(t0, t1, 10001)
104
+ xs = xfunc(td)
105
+ ys = yfunc(td)
106
+ XS, YS = _subsample_coordinates(xs, ys, tol, xmin)
107
+ return XS, YS
108
+
109
+ def rotate_point(point: tuple[float, float, float],
110
+ axis: tuple[float, float, float],
111
+ ang: float,
112
+ origin: tuple[float, float, float] = (0.0, 0.0, 0.0),
113
+ degrees: bool = False) -> tuple[float, float, float]:
114
+ """
115
+ Rotate a 3‑D point around an arbitrary axis that passes through `origin`.
116
+
117
+ Parameters
118
+ ----------
119
+ point : (x, y, z) coordinate of the point to rotate.
120
+ axis : (ux, uy, uz) direction vector of the rotation axis (need not be unit length).
121
+ ang : rotation angle. Positive values follow the right‑hand rule.
122
+ origin : (ox, oy, oz) point through which the axis passes. Defaults to global origin.
123
+ degrees : If True, `ang` is interpreted in degrees; otherwise in radians.
124
+
125
+ Returns
126
+ -------
127
+ (x,y,z) : tuple with the rotated coordinates.
128
+ """
129
+ # Convert inputs to numpy arrays
130
+ p = np.asarray(point, dtype=float)
131
+ o = np.asarray(origin, dtype=float)
132
+ u = np.asarray(axis, dtype=float)
133
+
134
+ # Shift so the axis passes through the global origin
135
+ p_shifted = p - o
136
+
137
+ # Normalise the axis direction
138
+ norm = np.linalg.norm(u)
139
+ if norm == 0:
140
+ raise ValueError("Axis direction vector must be non‑zero.")
141
+ u = u / norm
142
+
143
+ # Convert angle to radians if necessary
144
+ if degrees:
145
+ ang = np.radians(ang)
146
+
147
+ # Rodrigues’ rotation formula components
148
+ cos_a = np.cos(ang)
149
+ sin_a = np.sin(ang)
150
+ cross = np.cross(u, p_shifted)
151
+ dot = np.dot(u, p_shifted)
152
+
153
+ rotated = (p_shifted * cos_a
154
+ + cross * sin_a
155
+ + u * dot * (1 - cos_a))
156
+
157
+ # Shift back to original reference frame
158
+ rotated += o
159
+ return tuple(rotated)
160
+
161
+ class GeoPrism(GeoVolume):
162
+ """The GepPrism class generalizes the GeoVolume for extruded convex polygons.
163
+ Besides having a volumetric definitions, the class offers a .front_face
164
+ and .back_face property that selects the respective faces.
165
+
166
+ Args:
167
+ GeoVolume (_type_): _description_
168
+ """
169
+ def __init__(self,
170
+ volume_tag: int,
171
+ front_tag: int,
172
+ side_tags: list[int],):
173
+ super().__init__(volume_tag)
174
+ self.front_tag: int = front_tag
175
+ self.back_tag: int = None
176
+
177
+ gmsh.model.occ.synchronize()
178
+ o1 = gmsh.model.occ.get_center_of_mass(2, self.front_tag)
179
+ n1 = gmsh.model.get_normal(self.front_tag, (0,0))
180
+ self._add_face_pointer('back', o1, n1)
181
+
182
+ tags = gmsh.model.get_boundary(self.dimtags, oriented=False)
183
+
184
+ for dim, tag in tags:
185
+ if (dim,tag) in side_tags:
186
+ continue
187
+ o2 = gmsh.model.occ.get_center_of_mass(2, tag)
188
+ n2 = gmsh.model.get_normal(tag, (0,0))
189
+ self._add_face_pointer('front', o2, n2)
190
+ self.back_tag = tag
191
+ break
192
+
193
+ self.side_tags: list[int] = [dt[1] for dt in tags if dt[1]!=self.front_tag and dt[1]!=self.back_tag]
194
+
195
+ for tag in self.side_tags:
196
+ o2 = gmsh.model.occ.get_center_of_mass(2, tag)
197
+ n2 = gmsh.model.get_normal(tag, (0,0))
198
+ self._add_face_pointer(f'side{tag}', o2, n2)
199
+ self.back_tag = tag
200
+
201
+ def outside(self, *exclude: Literal['front','back']) -> FaceSelection:
202
+ """Select all outside faces except for the once specified by outside
203
+
204
+ Returns:
205
+ FaceSelection: The resultant face selection
206
+ """
207
+ tagslist = [self._face_tags(name) for name in self._face_pointers.keys() if name not in exclude]
208
+
209
+ tags = list(reduce(lambda a,b: a+b, tagslist))
210
+ return FaceSelection(tags)
211
+
212
+ class XYPolygon:
213
+ """This class generalizes a polygon in an un-embedded XY space that can be embedded in 3D space.
214
+ """
215
+ def __init__(self,
216
+ xs: np.ndarray | list | tuple = None,
217
+ ys: np.ndarray | list | tuple = None):
218
+ """Constructs an XY-plane placed polygon.
219
+
220
+ Args:
221
+ xs (np.ndarray): The X-points
222
+ ys (np.ndarray): The Y-points
223
+ """
224
+ if xs is None:
225
+ xs = []
226
+ if ys is None:
227
+ ys = []
228
+
229
+ self.x: np.ndarray = np.asarray(xs)
230
+ self.y: np.ndarray = np.asarray(ys)
231
+
232
+ self.fillets: list[tuple[float, int]] = []
233
+
234
+ @property
235
+ def N(self) -> int:
236
+ """The number of polygon points
237
+
238
+ Returns:
239
+ int: The number of points
240
+ """
241
+ return len(self.xs)
242
+
243
+ def _check(self) -> None:
244
+ """Checks if the last point is the same as the first point.
245
+ The XYPolygon does not store redundant points p[0]==p[N] so if these are
246
+ the same, this function will remove the last point.
247
+ """
248
+ if np.sqrt((self.x[-1]-self.x[0])**2 + (self.y[-1]-self.y[0])**2) < 1e-6:
249
+ self.x = self.x[:-1]
250
+ self.y = self.y[:-1]
251
+
252
+ @property
253
+ def area(self) -> float:
254
+ """The Area of the polygon
255
+
256
+ Returns:
257
+ float: The area in square meters
258
+ """
259
+ return 0.5*np.abs(np.dot(self.x,np.roll(self.y,1))-np.dot(self.y,np.roll(self.x,1)))
260
+
261
+ def extend(self, xpts: list[float], ypts: list[float]) -> XYPolygon:
262
+ """Adds a series for x and y coordinates to the existing polygon.
263
+
264
+ Args:
265
+ xpts (list[float]): The list of x-coordinates
266
+ ypts (list[float]): The list of y-coordinates
267
+
268
+ Returns:
269
+ XYPolygon: The same XYpolygon object
270
+ """
271
+ self.x = np.hstack([self.x, np.array(xpts)])
272
+ self.y = np.hstack([self.y, np.array(ypts)])
273
+ return self
274
+
275
+ def iterate(self) -> Generator[tuple[float, float],None, None]:
276
+ """ Iterates over the x,y coordinates as a tuple."""
277
+ for i in range(self.N):
278
+ yield (self.x[i], self.y[i])
279
+
280
+ def fillet(self, radius: float, *indices: int) -> None:
281
+ """Add a fillet rounding with a given radius to the provided nodes.
282
+
283
+ Example:
284
+ >>> my_polygon.fillet(0.05, 2, 3, 4, 6)
285
+
286
+ Args:
287
+ radius (float): The radius
288
+ *indices (int): The indices for which to apply the fillet.
289
+ """
290
+ for i in indices:
291
+ self.fillets.append((radius, i))
292
+
293
+ def _finalize(self, cs: CoordinateSystem = None) -> GeoPolygon:
294
+ """Turns the XYPolygon object into a GeoPolygon that is embedded in 3D space.
295
+
296
+ The polygon will be placed in the XY-plane of the provided coordinate center.
297
+
298
+ Args:
299
+ cs (CoordinateSystem, optional): The coordinate system in which to put the polygon. Defaults to None.
300
+
301
+ Returns:
302
+ GeoPolygon: The resultant 3D GeoPolygon object.
303
+ """
304
+ self._check()
305
+
306
+ ptags = []
307
+ xg, yg, zg = cs.in_global_cs(self.x, self.y, 0*self.x)
308
+
309
+ for x,y,z in zip(xg, yg, zg):
310
+ ptags.append(gmsh.model.occ.add_point(x,y,z))
311
+
312
+ lines = []
313
+ for i1, p1 in enumerate(ptags):
314
+ p2 = ptags[(i1+1) % len(ptags)]
315
+ lines.append(gmsh.model.occ.add_line(p1, p2))
316
+
317
+ add = 0
318
+ for radius, index in self.fillets:
319
+ t1 = lines[index + add]
320
+ t2 = lines[(index+add-1) % len(lines)]
321
+ tag = gmsh.model.occ.fillet2_d(t1, t2, radius)
322
+ lines.insert(index, tag)
323
+ add += 1
324
+
325
+ wiretag = gmsh.model.occ.add_wire(lines)
326
+ surftag = gmsh.model.occ.add_plane_surface([wiretag,])
327
+ poly = GeoPolygon([surftag,])
328
+ poly.points = ptags
329
+ poly.lines = lines
330
+ return poly
331
+
332
+ def extrude(self, length: float, cs: CoordinateSystem = None) -> GeoPrism:
333
+ """Extrues the polygon along the Z-axis.
334
+ The z-coordinates go from z1 to z2 (in meters). Then the extrusion
335
+ is either provided by a maximum dz distance (in meters) or a number
336
+ of sections N.
337
+
338
+ Args:
339
+ length (length): The length of the extrusion.
340
+
341
+ Returns:
342
+ GeoVolume: The resultant Volumetric object.
343
+ """
344
+ if cs is None:
345
+ cs = GCS
346
+ poly_fin = self._finalize(cs)
347
+ zax = length*cs.zax.np
348
+ poly_fin._exists = False
349
+ volume = gmsh.model.occ.extrude(poly_fin.dimtags, zax[0], zax[1], zax[2])
350
+ tags = [t for d,t in volume if d==3]
351
+ surftags = [t for d,t in volume if d==2]
352
+ return GeoPrism(tags, surftags[0], surftags)
353
+
354
+ def geo(self, cs: CoordinateSystem = None) -> GeoPolygon:
355
+ """Returns a GeoPolygon object for the current polygon.
356
+
357
+ Args:
358
+ cs (CoordinateSystem, optional): The Coordinate system of which the XY plane will be used. Defaults to None.
359
+
360
+ Returns:
361
+ GeoPolygon: The resultant object.
362
+ """
363
+ if cs is None:
364
+ cs = GCS
365
+ return self._finalize(cs)
366
+
367
+ def revolve(self, cs: CoordinateSystem, origin: tuple[float, float, float], axis: tuple[float, float,float], angle: float = 360.0) -> GeoPrism:
368
+ """Applies a revolution to the XYPolygon along the provided rotation ais
369
+
370
+ Args:
371
+ cs (CoordinateSystem, optional): _description_. Defaults to None.
372
+ angle (float, optional): _description_. Defaults to 360.0.
373
+
374
+ Returns:
375
+ Prism: The resultant
376
+ """
377
+ if cs is None:
378
+ cs = GCS
379
+ poly_fin = self._finalize(cs)
380
+
381
+ x,y,z = origin
382
+ ax, ay, az = axis
383
+
384
+ volume = gmsh.model.occ.revolve(poly_fin.dimtags, x,y,z, ax, ay, az, angle*np.pi/180)
385
+ tags = [t for d,t in volume if d==3]
386
+ surftags = [t for d,t in volume if d==2]
387
+ return GeoPrism(tags, surftags[0], surftags)
388
+
389
+ @staticmethod
390
+ def circle(radius: float,
391
+ dsmax: float = None,
392
+ tolerance: float = None,
393
+ Nsections: int = None):
394
+ """This method generates a segmented circle.
395
+
396
+ The number of points along the circumpherence can be specified in 3 ways. By a maximum
397
+ circumpherential length (dsmax), by a radial tolerance (tolerance) or by a number of
398
+ sections (Nsections).
399
+
400
+ Args:
401
+ radius (float): The circle radius
402
+ dsmax (float, optional): The maximum circumpherential angle. Defaults to None.
403
+ tolerance (float, optional): The maximum radial error. Defaults to None.
404
+ Nsections (int, optional): The number of sections. Defaults to None.
405
+
406
+ Returns:
407
+ XYPolygon: The XYPolygon object.
408
+ """
409
+ if Nsections is not None:
410
+ N = Nsections+1
411
+ elif dsmax is not None:
412
+ N = int(np.ceil((2*np.pi*radius)/dsmax))
413
+ elif tolerance is not None:
414
+ N = int(np.ceil(2*np.pi/np.arccos(1-tolerance)))
415
+
416
+ angs = np.linspace(0,2*np.pi,N)
417
+
418
+ xs = radius*np.cos(angs[:-1])
419
+ ys = radius*np.sin(angs[:-1])
420
+ return XYPolygon(xs, ys)
421
+
422
+ @staticmethod
423
+ def rect(width: float,
424
+ height: float,
425
+ origin: tuple[float, float],
426
+ alignment: Alignment = Alignment.CORNER) -> XYPolygon:
427
+ """Create a rectangle in the XY-plane as polygon
428
+
429
+ Args:
430
+ width (float): The width (X)
431
+ height (float): The height (Y)
432
+ origin (tuple[float, float]): The origin (x,y)
433
+ alignment (Alignment, optional): What point the origin describes. Defaults to Alignment.CORNER.
434
+
435
+ Returns:
436
+ XYPolygon: A new XYpolygon object
437
+ """
438
+ if alignment is Alignment.CORNER:
439
+ x0, y0 = origin
440
+ else:
441
+ x0 = origin[0]-width/2
442
+ y0 = origin[1]-height/2
443
+ xs = np.array([x0, x0, x0 + width, x0+width])
444
+ ys = np.array([y0, y0+height, y0+height, y0])
445
+ return XYPolygon(xs, ys)
446
+
447
+ def parametric(self, xfunc: Callable,
448
+ yfunc: Callable,
449
+ xmin: float = 1e-3,
450
+ tolerance: float = 1e-5,
451
+ tmin: float = 0,
452
+ tmax: float = 1,
453
+ reverse: bool = False) -> XYPolygon:
454
+ """Adds the points of a parametric curve to the polygon.
455
+ The parametric curve is defined by two parametric functions of a parameter t that (by default) lives in the interval from [0,1].
456
+ thus the curve x(t) = xfunc(t), and y(t) = yfunc(t).
457
+
458
+ The tolerance indicates a maximum deviation from the true path.
459
+
460
+ Args:
461
+ xfunc (Callable): The x-coordinate function.
462
+ yfunc (Callable): The y-coordinate function
463
+ tolerance (float): A maximum distance tolerance. Defaults to 10um.
464
+ tmin (float, optional): The start value of the t-parameter. Defaults to 0.
465
+ tmax (float, optional): The end value of the t-parameter. Defaults to 1.
466
+ reverse (bool, optional): Reverses the curve.
467
+
468
+ Returns:
469
+ XYPolygon: _description_
470
+ """
471
+ xs, ys = _discretize_curve(xfunc, yfunc, tmin, tmax, xmin, tolerance)
472
+
473
+ if reverse:
474
+ xs = xs[::-1]
475
+ ys = ys[::-1]
476
+ self.extend(xs, ys)
477
+ return self
478
+
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)