emerge 0.4.7__py3-none-any.whl → 0.4.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of emerge might be problematic. Click here for more details.

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.9.dist-info}/METADATA +7 -6
  74. emerge-0.4.9.dist-info/RECORD +78 -0
  75. emerge-0.4.9.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.9.dist-info}/WHEEL +0 -0
@@ -0,0 +1,510 @@
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
+ from .material import Material, AIR
21
+ from .selection import FaceSelection, DomainSelection, EdgeSelection, PointSelection
22
+ from loguru import logger
23
+ from typing import Literal, Any
24
+ import numpy as np
25
+
26
+ def _map_tags(tags: list[int], mapping: dict[int, list[int]]):
27
+ new_tags = []
28
+ for tag in tags:
29
+ new_tags.extend(mapping.get(tag, [tag,]))
30
+ return new_tags
31
+
32
+ def _bbcenter(x1, y1, z1, x2, y2, z2):
33
+ return np.array([(x1+x2)/2, (y1+y2)/2, (z1+z2)/2])
34
+ FaceNames = Literal['back','front','left','right','top','bottom']
35
+
36
+ class _KEY_GENERATOR:
37
+
38
+ def __init__(self):
39
+ self.start = -1
40
+
41
+ def new(self) -> int:
42
+ self.start += 1
43
+ return self.start
44
+
45
+ class _GeometryManager:
46
+
47
+ def __init__(self):
48
+ self.geometry_list: dict[str, list[GeoObject]] = dict()
49
+ self.active: str = ''
50
+
51
+ def all_geometries(self, model: str = None) -> list[GeoObject]:
52
+ if model is None:
53
+ model = self.active
54
+ return [geo for geo in self.geometry_list[model] if geo._exists]
55
+
56
+ def submit_geometry(self, geo: GeoObject, model: str = None) -> None:
57
+ if model is None:
58
+ model = self.active
59
+ self.geometry_list[model].append(geo)
60
+
61
+ def sign_in(self, modelname: str) -> None:
62
+ if modelname not in self.geometry_list:
63
+ self.geometry_list[modelname] = []
64
+ self.active = modelname
65
+
66
+ def reset(self, modelname: str) -> None:
67
+ self.geometry_list[modelname] = []
68
+
69
+ class _FacePointer:
70
+ """The FacePointer class defines a face to be selectable as a
71
+ face normal vector plus an origin. All faces of an object
72
+ can be selected based on the projected distance to the defined
73
+ selection plane of the center of mass of a face iff the normals
74
+ also align with some tolerance.
75
+
76
+ """
77
+ def __init__(self,
78
+ origin: np.ndarray,
79
+ normal: np.ndarray):
80
+ self.o = np.array(origin)
81
+ self.n = np.array(normal)
82
+
83
+ def find(self, dimtags: list[tuple[int,int]],
84
+ origins: list[np.ndarray],
85
+ normals: list[np.ndarray]) -> list[int]:
86
+ tags = []
87
+ for (d,t), o, n in zip(dimtags, origins, normals):
88
+ normdist = np.abs((o-self.o)@self.n)
89
+ dotnorm = np.abs(n@self.n)
90
+ if normdist < 1e-3 and dotnorm > 0.99:
91
+ tags.append(t)
92
+ return tags
93
+
94
+ def rotate(self, c0, ax, angle):
95
+ """
96
+ Rotate self.o and self.n about axis `ax`, centered at `c0`, by `angle` radians.
97
+
98
+ Parameters
99
+ ----------
100
+ c0 : np.ndarray
101
+ The center of rotation, shape (3,).
102
+ ax : np.ndarray
103
+ The axis to rotate around, shape (3,). Need not be unit length.
104
+ angle : float
105
+ Rotation angle in radians.
106
+ """
107
+ angle = -angle
108
+ # Ensure axis is a unit vector
109
+ k = ax / np.linalg.norm(ax)
110
+
111
+ # Precompute trig values
112
+ cos_theta = np.cos(angle)
113
+ sin_theta = np.sin(angle)
114
+
115
+ def rodrigues(v: np.ndarray) -> np.ndarray:
116
+ """
117
+ Rotate vector v around axis k by angle using Rodrigues' formula.
118
+ """
119
+ # term1 = v * cosθ
120
+ term1 = v * cos_theta
121
+ # term2 = (k × v) * sinθ
122
+ term2 = np.cross(k, v) * sin_theta
123
+ # term3 = k * (k ⋅ v) * (1 - cosθ)
124
+ term3 = k * (np.dot(k, v)) * (1 - cos_theta)
125
+ return term1 + term2 + term3
126
+
127
+ # Rotate the origin point about c0:
128
+ rel_o = self.o - c0 # move to rotation-centre coordinates
129
+ rot_o = rodrigues(rel_o) # rotate
130
+ self.o = rot_o + c0 # move back
131
+
132
+ # Rotate the normal vector (pure direction, no translation)
133
+ self.n = rodrigues(self.n)
134
+
135
+ def translate(self, dx, dy, dz):
136
+ self.o = self.o + np.array([dx, dy, dz])
137
+
138
+ def mirror(self, c0: np.ndarray, pln: np.ndarray):
139
+ """
140
+ Reflect self.o and self.n across the plane passing through c0
141
+ with normal pln.
142
+
143
+ Parameters
144
+ ----------
145
+ c0 : np.ndarray
146
+ A point on the mirror plane, shape (3,).
147
+ pln : np.ndarray
148
+ The normal of the mirror plane, shape (3,). Need not be unit length.
149
+ """
150
+ # Normalize the plane normal
151
+ k = pln / np.linalg.norm(pln)
152
+
153
+
154
+ # Reflect the origin point:
155
+ # compute vector from plane point to self.o
156
+ v_o = self.o - c0
157
+ # signed distance along normal
158
+ dist_o = np.dot(v_o, k)
159
+ # reflection
160
+ self.o = self.o - 2 * dist_o * k
161
+
162
+ # Reflect the normal/direction vector:
163
+ dist_n = np.dot(self.n, k)
164
+ self.n = self.n - 2 * dist_n * k
165
+
166
+ def affine_transform(self, M: np.ndarray):
167
+ """
168
+ Apply a 4×4 affine transformation matrix to both self.o and self.n.
169
+
170
+ Parameters
171
+ ----------
172
+ M : np.ndarray
173
+ The 4×4 affine transformation matrix.
174
+ - When applied to a point, use homogeneous w=1.
175
+ - When applied to a direction/vector, use homogeneous w=0.
176
+ """
177
+ # Validate shape
178
+ if M.shape != (4, 4):
179
+ raise ValueError(f"Expected M to be 4×4, got shape {M.shape}")
180
+
181
+ # Transform origin point (homogeneous w=1)
182
+ homo_o = np.empty(4)
183
+ homo_o[:3] = self.o
184
+ homo_o[3] = 1.0
185
+ transformed_o = M @ homo_o
186
+ self.o = transformed_o[:3]
187
+
188
+ # Transform normal/direction vector (homogeneous w=0)
189
+ homo_n = np.empty(4)
190
+ homo_n[:3] = self.n
191
+ homo_n[3] = 0.0
192
+ transformed_n = M @ homo_n
193
+ self.n = transformed_n[:3]
194
+ # Optionally normalize self.n if you need to keep it unit-length:
195
+ # self.n = self.n / np.linalg.norm(self.n)
196
+
197
+ def copy(self) -> _FacePointer:
198
+ return _FacePointer(self.o, self.n)
199
+
200
+
201
+ _GENERATOR = _KEY_GENERATOR()
202
+ _GEOMANAGER = _GeometryManager()
203
+
204
+ class GeoObject:
205
+ """A generalization of any OpenCASCADE entity described by a dimension and a set of tags.
206
+ """
207
+ dim: int = -1
208
+ def __init__(self):
209
+ self.old_tags: list[int] = []
210
+ self.tags: list[int] = []
211
+ self.material: Material = AIR
212
+ self.mesh_multiplier: float = 1.0
213
+ self.max_meshsize: float = 1e9
214
+
215
+ self._unset_constraints: bool = False
216
+ self._embeddings: list[GeoObject] = []
217
+ self._face_pointers: dict[str, _FacePointer] = dict()
218
+ self._tools: dict[int, dict[str, _FacePointer]] = dict()
219
+
220
+ self._key = _GENERATOR.new()
221
+ self._aux_data: dict[str, Any] = dict()
222
+ self._priority: int = 10
223
+
224
+ self._exists: bool = True
225
+ _GEOMANAGER.submit_geometry(self)
226
+
227
+ @property
228
+ def color_rgb(self) -> tuple[int,int,int]:
229
+ return self.material.color_rgb
230
+
231
+ @property
232
+ def opacity(self) -> float:
233
+ return self.material.opacity
234
+
235
+ @property
236
+ def select(self) -> FaceSelection | DomainSelection | EdgeSelection | None:
237
+ '''Returns a corresponding Face/Domain or Edge Selection object'''
238
+ if self.dim==1:
239
+ return EdgeSelection(self.tags)
240
+ elif self.dim==2:
241
+ return FaceSelection(self.tags)
242
+ elif self.dim==3:
243
+ return DomainSelection(self.tags)
244
+
245
+ @staticmethod
246
+ def merged(objects: list[GeoObject]) -> list[GeoObject]:
247
+ dim = objects[0].dim
248
+ tags = []
249
+ for obj in objects:
250
+ tags.extend(obj.tags)
251
+ if dim==2:
252
+ out = GeoSurface(tags)
253
+ elif dim==3:
254
+ out = GeoVolume(tags)
255
+ else:
256
+ out = GeoObject(tags)
257
+ out.material = objects[0].material
258
+ return out
259
+
260
+ def __repr__(self) -> str:
261
+ return f'{self.__class__.__name__}({self.dim},{self.tags})'
262
+
263
+ def _data(self, *labels) -> tuple[Any]:
264
+ return tuple([self._aux_data.get(lab, None) for lab in labels])
265
+
266
+ def _add_face_pointer(self,
267
+ name: str,
268
+ origin: np.ndarray,
269
+ normal: np.ndarray):
270
+ self._face_pointers[name] = _FacePointer(origin, normal)
271
+
272
+ def make_copy(self) -> GeoObject:
273
+ new_dimtags = gmsh.model.occ.copy(self.dimtags)
274
+ new_obj = GeoObject.from_dimtags(new_dimtags)
275
+ new_obj.material = self.material
276
+ new_obj.mesh_multiplier = self.mesh_multiplier
277
+ new_obj.max_meshsize = self.max_meshsize
278
+
279
+ new_obj._unset_constraints = self._unset_constraints
280
+ new_obj._embeddings = [emb.make_copy() for emb in self._embeddings]
281
+ new_obj._face_pointers = {key: value.copy() for key,value in self._face_pointers.items()}
282
+ new_obj._tools = {key: {key2: value2.copy() for key2, value2 in value.items()} for key,value in self._tools.items()}
283
+
284
+ new_obj._aux_data = self._aux_data.copy()
285
+ new_obj._priority = self._priority
286
+ new_obj._exists = self._exists
287
+ return new_obj
288
+
289
+ def replace_tags(self, tagmap: dict[int, list[int]]):
290
+ self.old_tags = self.tags
291
+ newtags = []
292
+ for tag in self.tags:
293
+ newtags.extend(tagmap.get(tag, [tag,]))
294
+ self.tags = newtags
295
+ logger.debug(f'Replaced {self.old_tags} with {self.tags}')
296
+
297
+ def update_tags(self, tag_mapping: dict[int,dict]) -> GeoObject:
298
+ ''' Update the tag definition of a GeoObject after fragementation.'''
299
+ self.replace_tags(tag_mapping[self.dim])
300
+ return self
301
+
302
+ def _take_pointers(self, *others: GeoObject) -> GeoObject:
303
+ for other in others:
304
+ self._face_pointers.update(other._face_pointers)
305
+ self._tools.update(other._tools)
306
+ return self
307
+
308
+ @property
309
+ def _all_pointers(self) -> list[_FacePointer]:
310
+ pointers = list(self._face_pointers.values())
311
+ for dct in self._tools.values():
312
+ pointers.extend(list(dct.values()))
313
+ return pointers
314
+
315
+ @property
316
+ def _all_pointer_names(self) -> set[str]:
317
+ keys = set(self._face_pointers.keys())
318
+ for dct in self._tools.values():
319
+ keys = keys.union(set(dct.keys()))
320
+ return keys
321
+
322
+ def _take_tools(self, *objects: GeoObject) -> GeoObject:
323
+ for obj in objects:
324
+ self._tools[obj._key] = obj._face_pointers
325
+ self._tools.update(obj._tools)
326
+ return self
327
+
328
+ def _face_tags(self, name: FaceNames, tool: GeoObject = None) -> list[int]:
329
+ names = self._all_pointer_names
330
+ if name not in names:
331
+ raise ValueError(f'The face {name} does not exist in {self}')
332
+
333
+ gmsh.model.occ.synchronize()
334
+ dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
335
+
336
+ normals = [gmsh.model.get_normal(t, [0,0]) for d,t, in dimtags]
337
+ origins = [gmsh.model.occ.get_center_of_mass(d, t) for d,t in dimtags]
338
+
339
+ if tool is not None:
340
+ tags = self._tools[tool._key][name].find(dimtags, origins, normals)
341
+ else:
342
+ tags = self._face_pointers[name].find(dimtags, origins, normals)
343
+ logger.info(f'Selected face {tags}.')
344
+ return tags
345
+
346
+ def set_material(self, material: Material) -> GeoObject:
347
+ self.material = material
348
+ return self
349
+
350
+ def prio_set(self, level: int) -> GeoObject:
351
+ """Defines the material assignment priority level of this geometry.
352
+ By default all objects have priority level 10. If you assign a lower number,
353
+ in cases where multiple geometries occupy the same volume, the highest priority
354
+ will be chosen.
355
+
356
+ Args:
357
+ level (int): The priority level
358
+
359
+ Returns:
360
+ GeoObject: The same object
361
+ """
362
+ self._priority = level
363
+ return self
364
+
365
+ def prio_up(self) -> GeoObject:
366
+ """Increase priority by 1
367
+
368
+ Returns:
369
+ GeoObject: _description_
370
+ """
371
+ self._priority += 1
372
+ return self
373
+
374
+ def prio_down(self) -> GeoObject:
375
+ """Decrase priority by 1
376
+
377
+ Returns:
378
+ GeoObject: _description_
379
+ """
380
+ self._priority -= 1
381
+ return self
382
+
383
+ def outside(self, *exclude: FaceNames, tags: list[int] = None) -> FaceSelection:
384
+ """Returns the complete set of outside faces.
385
+
386
+ If implemented, it is possible to exclude a set of faces based on their name
387
+ or a list of tags (integers)
388
+
389
+ Returns:
390
+ FaceSelection: The selected faces
391
+ """
392
+ if tags is None:
393
+ tags = []
394
+ dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
395
+ return FaceSelection([t for d,t in dimtags if t not in tags])
396
+
397
+ def face(self, name: FaceNames, tool: GeoObject = None) -> FaceSelection:
398
+ """Returns the FaceSelection for a given face name.
399
+
400
+ The face name must be defined for the type of geometry.
401
+
402
+ Args:
403
+ name (FaceNames): The name of the face to select.
404
+
405
+ Returns:
406
+ FaceSelection: The selected face
407
+ """
408
+
409
+ return FaceSelection(self._face_tags(name, tool))
410
+
411
+ @property
412
+ def dimtags(self) -> list[tuple[int, int]]:
413
+ return [(self.dim, tag) for tag in self.tags]
414
+
415
+ @property
416
+ def embeddings(self) -> list[tuple[int,int]]:
417
+ return []
418
+
419
+ def boundary(self) -> FaceSelection:
420
+ if self.dim == 3:
421
+ tags = gmsh.model.get_boundary(self.dimtags, oriented=False)
422
+ return FaceSelection([t[1] for t in tags])
423
+ if self.dim == 2:
424
+ return FaceSelection(self.tags)
425
+ if self.dim < 2:
426
+ raise ValueError('Can only generate faces for objects of dimension 2 or higher.')
427
+
428
+ @staticmethod
429
+ def from_dimtags(dimtags: list[tuple[int,int]]) -> GeoVolume | GeoSurface | GeoObject:
430
+ dim = dimtags[0][0]
431
+ tags = [t for d,t in dimtags]
432
+ if dim==0:
433
+ return GeoPoint(tags)
434
+ elif dim==1:
435
+ return GeoEdge(tags)
436
+ if dim==2:
437
+ return GeoSurface(tags)
438
+ if dim==3:
439
+ return GeoVolume(tags)
440
+ return GeoObject(tags)
441
+
442
+ class GeoVolume(GeoObject):
443
+ '''GeoVolume is an interface to the GMSH CAD kernel. It does not represent EMerge
444
+ specific geometry data.'''
445
+ dim = 3
446
+ def __init__(self, tag: int | list[int]):
447
+ super().__init__()
448
+ if isinstance(tag, list):
449
+ self.tags: list[int] = tag
450
+ else:
451
+ self.tags: list[int] = [tag,]
452
+
453
+ @property
454
+ def select(self) -> DomainSelection:
455
+ return DomainSelection(self.tags)
456
+
457
+ class GeoPoint(GeoObject):
458
+ dim = 0
459
+
460
+ @property
461
+ def select(self) -> PointSelection:
462
+ return PointSelection(self.tags)
463
+
464
+ def __init__(self, tag: int | list[int]):
465
+ super().__init__()
466
+ if isinstance(tag, list):
467
+ self.tags: list[int] = tag
468
+ else:
469
+ self.tags: list[int] = [tag,]
470
+
471
+ class GeoEdge(GeoObject):
472
+ dim = 1
473
+
474
+ @property
475
+ def select(self) -> EdgeSelection:
476
+ return EdgeSelection(self.tags)
477
+
478
+ def __init__(self, tag: int | list[int]):
479
+ super().__init__()
480
+ if isinstance(tag, list):
481
+ self.tags: list[int] = tag
482
+ else:
483
+ self.tags: list[int] = [tag,]
484
+
485
+
486
+ class GeoSurface(GeoObject):
487
+ '''GeoVolume is an interface to the GMSH CAD kernel. It does not reprsent Emerge
488
+ specific geometry data.'''
489
+ dim = 2
490
+
491
+ @property
492
+ def select(self) -> FaceSelection:
493
+ return FaceSelection(self.tags)
494
+
495
+ def __init__(self, tag: int | list[int]):
496
+ super().__init__()
497
+ if isinstance(tag, list):
498
+ self.tags: list[int] = tag
499
+ else:
500
+ self.tags: list[int] = [tag,]
501
+
502
+ class GeoPolygon(GeoSurface):
503
+
504
+ def __init__(self,
505
+ tags: list[int]):
506
+ super().__init__(tags)
507
+ self.points: list[int] = None
508
+ self.lines: list[int] = None
509
+
510
+