emerge 1.1.0__py3-none-any.whl → 1.1.1__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.

@@ -1,11 +1,70 @@
1
1
  import gmsh
2
2
  from ..geometry import GeoPoint, GeoEdge, GeoVolume, GeoSurface, GeoObject
3
+ from ..selection import FaceSelection
4
+ from .shapes import Box
5
+ from .operations import unite
6
+
3
7
  from pathlib import Path
4
8
  import numpy as np
9
+ from typing import Callable
5
10
 
6
- class STEPItems:
11
+ def _select_num(num1: float | None, num2: float | None) -> float:
12
+ if num1 is None and num2 is None:
13
+ return 0.0
14
+ if isinstance(num1, float) and num2 is None:
15
+ return num1
16
+ if num1 is None and isinstance(num2, float):
17
+ return num2
18
+ return max(num1, num2)
19
+
20
+ class _FaceSliceSelector:
21
+
22
+ def __init__(self, face_numbers: list[int], selector: Callable):
23
+ self.numbers: list[str] = face_numbers
24
+ self.selector: Callable = selector
25
+
26
+ def __getitem__(self, slice) -> FaceSelection:
27
+ nums = self.numbers.__getitem__(slice)
28
+ if isinstance(nums, int):
29
+ nums = [nums,]
30
+ return self.selector(*nums)
31
+
32
+ class StepVolume(GeoVolume):
33
+ """The StepVoume class extens the EMerge GeoVolume class to add easier
34
+ face selection functionalities based on numbers as face names are not
35
+ imported currently
36
+
37
+ Args:
38
+ GeoVolume (_type_): _description_
39
+
40
+ Returns:
41
+ _type_: _description_
42
+ """
43
+
44
+
45
+ @property
46
+ def _face_numbers(self) -> list[int]:
47
+ return sorted([int(name[4:]) for name in self._face_pointers.keys()])
48
+
49
+ @property
50
+ def face_slice(self) -> _FaceSliceSelector:
51
+ return _FaceSliceSelector(self._face_numbers, self.faces)
52
+
53
+ def faces(self, *numbers: int) -> FaceSelection:
54
+ """Select a set of faces by number
55
+
56
+ Returns:
57
+ FaceSelection: _description_
58
+ """
59
+ names = [f'Face{num}' for num in numbers]
60
+ return super().faces(*names)
61
+
7
62
 
8
- def __init__(self, filename: str, unit: float = 1.0):
63
+ class STEPItems:
64
+ """STEPItems imports geometries form a STEP file and exposes them to the user.
65
+
66
+ """
67
+ def __init__(self, name: str, filename: str, unit: float = 1.0):
9
68
  """Imports the provided STEP file.
10
69
  Specify the unit in case of scaling issues where mm units are not taken into consideration.
11
70
 
@@ -16,62 +75,139 @@ class STEPItems:
16
75
  Raises:
17
76
  FileNotFoundError: If a file does not exist
18
77
  """
78
+ self.name: str = name
79
+
19
80
  stl_path = Path(filename)
20
- gmsh.option.setNumber("Geometry.OCCScaling", 1)
21
- gmsh.option.setNumber("Geometry.OCCImportLabels", 2)
81
+ gmsh.option.setNumber("Geometry.OCCScaling", unit)
82
+ gmsh.option.setNumber("Geometry.OCCImportLabels", 1)
22
83
 
23
84
  if not stl_path.exists:
24
85
  raise FileNotFoundError(f'File with name {stl_path} does not exist.')
25
86
 
26
87
  dimtags = gmsh.model.occ.import_shapes(filename, format='step')
27
-
28
- gmsh.model.occ.affine_transform(dimtags, np.array([unit, 0, 0, 0,
29
- 0, unit, 0, 0,
30
- 0, 0, unit, 0,
31
- 0, 0, 0, 1]))
32
- #dimtags = gmsh.model.occ.heal_shapes(dimtags, tolerance=1e-6)
33
88
 
34
- self.points: dict[str, GeoPoint] = dict()
35
- self.edges: dict[str, GeoEdge] = dict()
36
- self.surfaces: dict[str, GeoSurface] = dict()
37
- self.volumes: dict[str, GeoVolume] = dict()
38
-
89
+ self.points: list[GeoPoint] = []
90
+ self.edges: list[GeoEdge] = []
91
+ self.surfaces: list[GeoSurface] = []
92
+ self.volumes: list[GeoVolume] = []
93
+
39
94
  i = 0
40
95
  for dim, tag in dimtags:
41
- name = gmsh.model.getPhysicalName(dim, tag)
96
+ name = gmsh.model.getPhysicalName(dim, tag) #for now, this doesn't actually ever work.
42
97
  if name == '':
43
98
  name = f'Obj{i}'
44
99
  i+=1
45
100
  if dim == 0:
46
- self.points[name] = GeoPoint(tag)
101
+ self.points.append(GeoPoint(tag, name=f'{self.name}_{name}'))
47
102
  elif dim == 1:
48
- self.edges[name] = GeoEdge(tag)
103
+ self.edges.append(GeoEdge(tag, name=f'{self.name}_{name}'))
49
104
  elif dim == 2:
50
- self.surfaces[name] = GeoSurface(tag)
105
+ self.surfaces.append(GeoSurface(tag, name=f'{self.name}_{name}'))
51
106
  elif dim == 3:
52
- self.volumes[name] = GeoVolume(tag)
53
-
107
+ self.volumes.append(StepVolume(tag, name=f'{self.name}_{name}'))
108
+
109
+ gmsh.model.occ.synchronize()
110
+
54
111
  @property
55
- def _dicts(self):
56
- yield self.points
57
- yield self.edges
58
- yield self.surfaces
59
- yield self.volumes
60
-
112
+ def dictionary(self) -> dict[str, GeoObject]:
113
+ return {obj.name: obj for obj in self.objects}
114
+
61
115
  @property
62
116
  def objects(self) -> tuple[GeoObject,...]:
63
- objects = tuple()
64
- for dct in self._dicts:
65
- objects = objects + tuple(dct.values())
66
- return objects
117
+ """Returns a list of all objects in the STEP file
118
+
119
+ Returns:
120
+ tuple[GeoObject,...]: _description_
121
+ """
122
+ return tuple(self.points+self.edges+self.surfaces+self.volumes)
123
+
124
+ def __getitem__(self, name: str) -> GeoObject | None:
125
+ return self.dictionary.get(name, None)
126
+
127
+ def as_volume(self) -> StepVolume:
128
+ """Returns the 3D volumetric part of the STEP file as a single geometry
129
+
130
+ Returns:
131
+ StepVolume: The resultant StepVolume(GeoVolume) object.
132
+ """
133
+ if len(self.volumes)==1:
134
+ return self.volumes[0]
135
+ return unite(*self.volumes)._auto_face_tag()
136
+
137
+ def as_surface(self) -> GeoSurface:
138
+ """Returns the 2D surface part of the STEP file as a single geometry
139
+
140
+
141
+ Returns:
142
+ GeoSurface: The resultant GeoSurface object
143
+ """
144
+ if len(self.surfaces)==1:
145
+ return self.surfaces[0]
146
+ return unite(*self.surfaces)
147
+
148
+ def as_edge(self) -> GeoEdge:
149
+ """Returns the 1D Edge part of the STEP file as a single geometry
150
+
151
+ Returns:
152
+ GeoEdge: The resultant GeoEdge object
153
+ """
154
+ if len(self.edges)==1:
155
+ return self.edges[1]
156
+ return unite(*self.edges)
67
157
 
68
- def __getitem__(self, name: str) -> GeoObject:
69
- if name in self.points:
70
- return self.points[name]
71
- elif name in self.edges:
72
- return self.edges[name]
73
- elif name in self.surfaces:
74
- return self.surfaces[name]
75
- elif name in self.volumes:
76
- return self.volumes[name]
77
-
158
+ def as_point(self) -> GeoPoint:
159
+ """Returns the 0D Point part of the STEP file as a single geometry
160
+
161
+ Returns:
162
+ GeoPoint: The resultant GeoPoint object.
163
+ """
164
+ if len(self.points)==1:
165
+ return self.points[0]
166
+ return unite(*self.points)
167
+
168
+ def enclose(self,
169
+ margin: float = None,
170
+ x_margins: tuple[float, float] = (None, None),
171
+ y_margins: tuple[float, float] = (None, None),
172
+ z_margins: tuple[float, float] = (None, None)) -> Box:
173
+ """Create an enclosing bounding box for the step model.
174
+
175
+ Args:
176
+ margin (float, optional): _description_. Defaults to 0.
177
+ x_margins (tuple[float, float], optional): _description_. Defaults to (0., 0.).
178
+ y_margins (tuple[float, float], optional): _description_. Defaults to (0., 0.).
179
+ z_margins (tuple[float, float], optional): _description_. Defaults to (0., 0.).
180
+
181
+ Returns:
182
+ Box: _description_
183
+ """
184
+ xminm = _select_num(margin, x_margins[0])
185
+ xmaxm = _select_num(margin, x_margins[1])
186
+ yminm = _select_num(margin, y_margins[0])
187
+ ymaxm = _select_num(margin, y_margins[1])
188
+ zminm = _select_num(margin, z_margins[0])
189
+ zmaxm = _select_num(margin, z_margins[1])
190
+
191
+ xmin = 1000000
192
+ xmax = -1000000
193
+ ymin = 1000000
194
+ ymax = -1000000
195
+ zmin = 1000000
196
+ zmax = -1000000
197
+
198
+ for obj in self.objects:
199
+ for dim, tag in obj.dimtags:
200
+ x1, y1, z1, x2, y2, z2 = gmsh.model.occ.getBoundingBox(dim, tag)
201
+ xmin = min(xmin, x1)
202
+ xmax = max(xmax, x2)
203
+ ymin = min(ymin, y1)
204
+ ymax = max(ymax, y2)
205
+ zmin = min(zmin, z1)
206
+ zmax = max(zmax, z2)
207
+
208
+ width = xmax-xmin + xminm + xmaxm
209
+ depth = ymax-ymin + yminm + ymaxm
210
+ height = zmax -zmin + zminm + zmaxm
211
+
212
+ return Box(width, depth, height, (xmin-xminm, ymin-yminm, zmin-zminm)).background()
213
+
@@ -18,13 +18,12 @@
18
18
  from __future__ import annotations
19
19
  import gmsh # type: ignore
20
20
  from .material import Material, AIR
21
- from .selection import FaceSelection, DomainSelection, EdgeSelection, PointSelection, Selection
21
+ from .selection import FaceSelection, DomainSelection, EdgeSelection, PointSelection, Selection, SelectionError
22
22
  from loguru import logger
23
- from typing import Literal, Any, Iterable, TypeVar
23
+ from typing import Literal, Any, Iterable, Callable
24
24
  import numpy as np
25
25
 
26
26
 
27
-
28
27
  def _map_tags(tags: list[int], mapping: dict[int, list[int]]):
29
28
  new_tags = []
30
29
  for tag in tags:
@@ -34,6 +33,7 @@ def _map_tags(tags: list[int], mapping: dict[int, list[int]]):
34
33
  def _bbcenter(x1, y1, z1, x2, y2, z2):
35
34
  return np.array([(x1+x2)/2, (y1+y2)/2, (z1+z2)/2])
36
35
 
36
+
37
37
  FaceNames = Literal['back','front','left','right','top','bottom']
38
38
 
39
39
  class _KEY_GENERATOR:
@@ -232,6 +232,8 @@ class _FacePointer:
232
232
  def copy(self) -> _FacePointer:
233
233
  return _FacePointer(self.o, self.n)
234
234
 
235
+ def __eq__(self, other: _FacePointer) -> bool:
236
+ return (np.linalg.norm(self.o - other.o) + np.linalg.norm(self.n - other.n)) < 1e-6
235
237
 
236
238
  _GENERATOR = _KEY_GENERATOR()
237
239
  _GEOMANAGER = _GeometryManager()
@@ -241,6 +243,7 @@ class GeoObject:
241
243
  """
242
244
  dim: int = -1
243
245
  _default_name: str = 'GeoObject'
246
+
244
247
  def __init__(self, tags: list[int] | None = None, name: str | None = None):
245
248
  if tags is None:
246
249
  tags = []
@@ -263,7 +266,24 @@ class GeoObject:
263
266
 
264
267
  self.give_name(name)
265
268
  _GEOMANAGER.submit_geometry(self)
266
-
269
+ self._fill_face_pointers()
270
+
271
+ def _fill_face_pointers(self) -> None:
272
+ """ Fills the list of all face pointers of this object
273
+ """
274
+ current = list(self._face_pointers.values())
275
+ ctr = 0
276
+ gmsh.model.occ.synchronize()
277
+ for dim, tag in gmsh.model.get_boundary(self.dimtags, True, False):
278
+ if dim != 2:
279
+ continue
280
+ o = gmsh.model.occ.get_center_of_mass(2, tag)
281
+ n = gmsh.model.get_normal(tag, (0,0))
282
+ fp = _FacePointer(o, n)
283
+ if fp not in current:
284
+ self._face_pointers[f'Face{ctr}'] = fp
285
+ ctr += 1
286
+
267
287
  def _store(self, name: str, data: Any) -> None:
268
288
  """Store a property as auxilliary data under a given name
269
289
 
@@ -361,6 +381,19 @@ class GeoObject:
361
381
  def _data(self, *labels) -> tuple[Any | None, ...]:
362
382
  return tuple([self._aux_data[lab] for lab in labels])
363
383
 
384
+ def _replace_pointer(self, name: str, face_pointer: _FacePointer) -> None:
385
+ """Will be used to replace face pointers so only one unique one exists.
386
+
387
+ Args:
388
+ name (str): _description_
389
+ face_pointer (_FacePointer): _description_
390
+ """
391
+ for key, fp in self._face_pointers.items():
392
+ if fp == face_pointer:
393
+ self._face_pointers.pop(key)
394
+ break
395
+ self._face_pointers[name] = face_pointer
396
+
364
397
  def _add_face_pointer(self,
365
398
  name: str,
366
399
  origin: np.ndarray | None = None,
@@ -378,15 +411,16 @@ class GeoObject:
378
411
  ValueError: _description_
379
412
  """
380
413
  if tag is not None:
381
- o = gmsh.model.occ.get_center_of_mass(2, tag)
382
- n = gmsh.model.get_normal(tag, (0,0))
383
- self._face_pointers[name] = _FacePointer(o, n)
384
- return
385
- if origin is not None and normal is not None:
386
- self._face_pointers[name] = _FacePointer(origin, normal)
387
- return
388
- raise ValueError('Eitehr a tag or an origin + normal must be provided!')
389
-
414
+ origin = gmsh.model.occ.get_center_of_mass(2, tag)
415
+ normal = gmsh.model.get_normal(tag, (0,0))
416
+
417
+ if origin is None or normal is None:
418
+ raise ValueError('Eitehr a tag or an origin + normal must be provided!')
419
+ fp = _FacePointer(origin, normal)
420
+ self._face_pointers[name] = fp
421
+ #self._replace_pointer(name, fp) <-- Will be added in later versions
422
+
423
+
390
424
  def make_copy(self) -> GeoObject:
391
425
  """ Copies this object and returns a new object (also in GMSH)"""
392
426
  new_dimtags = gmsh.model.occ.copy(self.dimtags)
@@ -452,7 +486,7 @@ class GeoObject:
452
486
  def _face_tags(self, name: FaceNames, tool: GeoObject | None = None) -> list[int]:
453
487
  names = self._all_pointer_names
454
488
  if name not in names:
455
- raise ValueError(f'The face {name} does not exist in {self}')
489
+ raise ValueError(f'The face {name} does not exist in {self}. Only {list(self._face_pointers.keys())}')
456
490
 
457
491
  gmsh.model.occ.synchronize()
458
492
  dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
@@ -467,6 +501,7 @@ class GeoObject:
467
501
  logger.info(f'Selected face {tags}.')
468
502
  return tags
469
503
 
504
+
470
505
  def set_material(self, material: Material) -> GeoObject:
471
506
  self.material = material
472
507
  return self
@@ -563,6 +598,9 @@ class GeoObject:
563
598
  Returns:
564
599
  FaceSelection: The selected faces
565
600
  """
601
+ # if not self._exists:
602
+ # raise SelectionError('Cannot select faces from an object that no longer exists.')
603
+
566
604
  if isinstance(exclude, str):
567
605
  exclude = (exclude,)
568
606
 
@@ -572,11 +610,13 @@ class GeoObject:
572
610
  if tags is None:
573
611
  tags = []
574
612
 
575
-
576
613
  for name in exclude:
577
614
  tags.extend(self.face(name, tool=tool).tags)
578
615
  dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
579
- return FaceSelection([t for d,t in dimtags if t not in tags])
616
+ selname = 'Boundary'
617
+ if exclude:
618
+ selname = selname + 'Except[' + ','.join(exclude) + ']'
619
+ return FaceSelection([t for d,t in dimtags if t not in tags])._named(selname)
580
620
 
581
621
  def face(self, name: FaceNames = None, tool: GeoObject | None = None, no: FaceNames = None) -> FaceSelection:
582
622
  """Returns the FaceSelection for a given face name.
@@ -596,8 +636,16 @@ class GeoObject:
596
636
  if no is not None:
597
637
  return self.boundary(exclude=no)
598
638
 
599
- return FaceSelection(self._face_tags(name, tool))
639
+ return FaceSelection(self._face_tags(name, tool))._named(name)
640
+
641
+ def all_faces(self) -> list[FaceSelection]:
642
+ """Returns a list of all face selections of this object
600
643
 
644
+ Returns:
645
+ list[FaceSelection]: A list of all face selections
646
+ """
647
+ return [self.face(name) for name in self._face_pointers]
648
+
601
649
  def faces(self, *names: FaceNames, tool: GeoObject | None = None) -> FaceSelection:
602
650
  """Returns the FaceSelection for a given face names.
603
651
 
@@ -613,7 +661,7 @@ class GeoObject:
613
661
  tags = []
614
662
  for name in names:
615
663
  tags.extend(self._face_tags(name, tool))
616
- return FaceSelection(tags)
664
+ return FaceSelection(tags)._named('Faces[' + ','.join(names) + ']')
617
665
 
618
666
  def hide(self) -> GeoObject:
619
667
  """Hides the object from views
@@ -658,7 +706,22 @@ class GeoObject:
658
706
  def remove(self) -> None:
659
707
  self._exists = False
660
708
  gmsh.model.occ.remove(self.dimtags, True)
661
-
709
+
710
+ def extract(self, tags: int | list[int]) -> GeoObject:
711
+ """Returns a new GeoObject of the same dimensional type that isolates a set of given tags
712
+
713
+ Args:
714
+ tags (list[int]): A list of GMSH tags
715
+
716
+ Returns:
717
+ GeoObject: _description_
718
+ """
719
+ if isinstance(tags, int):
720
+ tags = [tags,]
721
+ self.tags = [t for t in self.tags if t not in tags]
722
+ dts = [(self.dim, t) for t in tags]
723
+ return GeoObject.from_dimtags(dts)
724
+
662
725
  class GeoVolume(GeoObject):
663
726
  '''GeoVolume is an interface to the GMSH CAD kernel. It does not represent EMerge
664
727
  specific geometry data.'''
@@ -675,10 +738,94 @@ class GeoVolume(GeoObject):
675
738
  else:
676
739
  self.tags = [tag,]
677
740
 
741
+ self._fill_face_pointers()
742
+ self._autoname()
743
+
678
744
  @property
679
745
  def selection(self) -> DomainSelection:
680
746
  return DomainSelection(self.tags)
681
747
 
748
+ def _auto_face_tag(self) -> GeoVolume:
749
+ self._face_pointers = dict()
750
+ self._tools = dict()
751
+ logger.trace('Automatically assigning face pointers.')
752
+ ctr = 0
753
+ for tag in self.tags:
754
+ loops = gmsh.model.occ.getSurfaceLoops(tag)
755
+ for loopcluster in loops[1]:
756
+ for st in loopcluster:
757
+ self._add_face_pointer(f'Face{ctr}', tag=st)
758
+ ctr += 1
759
+ return self
760
+
761
+ def exterior_faces(self, base_object: GeoObject) -> FaceSelection:
762
+ """Select the exterior faces of an object based on the face
763
+ pointers of an original base object. For example
764
+
765
+ Example:
766
+ >>> cheese = em.geo.Box(...)
767
+ >>> hole = em.geo.Box(...)
768
+ >>> holed_cheese = em.geo.subtract(cheese, hole)
769
+ >>> holed_cheese.exterior_faces(cheese)
770
+
771
+ Args:
772
+ base_object (GeoObject): The object to base the selection on.
773
+
774
+ Returns:
775
+ FaceSelection: The resultant face selection
776
+ """
777
+ dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
778
+
779
+ normals = [gmsh.model.get_normal(t, [0,0]) for d,t, in dimtags]
780
+ origins = [gmsh.model.occ.get_center_of_mass(d, t) for d,t in dimtags]
781
+
782
+ tool_tags = []
783
+ for key, tool in self._tools.items():
784
+ if key == base_object._key:
785
+ continue
786
+ for name in tool.keys():
787
+ tags = tool[name].find(dimtags, origins, normals)
788
+ tool_tags.extend(tags)
789
+ tags = [dt[1] for dt in dimtags if dt[1] not in tool_tags]
790
+ return FaceSelection(tags)
791
+
792
+ def _find_fp(self, condition: Callable):
793
+ for key, fp in self._face_pointers.items():
794
+ if condition(fp):
795
+ return key, fp
796
+ return None, None
797
+
798
+ def _rename_fp(self, new_name: str, condition: Callable):
799
+ name, fp = self._find_fp(condition)
800
+ if fp is not None:
801
+ self._face_pointers.pop(name)
802
+ self._face_pointers[new_name] = fp
803
+
804
+ def _autoname(self) -> None:
805
+ if len(self._face_pointers)==0:
806
+ return
807
+
808
+ xs = []
809
+ ys = []
810
+ zs = []
811
+ for fp in self._face_pointers.values():
812
+ xs.append(fp.o[0])
813
+ ys.append(fp.o[1])
814
+ zs.append(fp.o[2])
815
+ minx = min(xs)
816
+ maxx = max(xs)
817
+ miny = min(ys)
818
+ maxy = max(ys)
819
+ minz = min(zs)
820
+ maxz = max(zs)
821
+
822
+ self._rename_fp('-x', lambda fp: (fp.o[0]==minx) and np.abs(np.dot(fp.n, np.array([1,0,0])))>0.999)
823
+ self._rename_fp('+x', lambda fp: (fp.o[0]==maxx) and np.abs(np.dot(fp.n, np.array([1,0,0])))>0.999)
824
+ self._rename_fp('-y', lambda fp: (fp.o[1]==miny) and np.abs(np.dot(fp.n, np.array([0,1,0])))>0.999)
825
+ self._rename_fp('+y', lambda fp: (fp.o[1]==maxy) and np.abs(np.dot(fp.n, np.array([0,1,0])))>0.999)
826
+ self._rename_fp('-z', lambda fp: (fp.o[2]==minz) and np.abs(np.dot(fp.n, np.array([0,0,1])))>0.999)
827
+ self._rename_fp('+z', lambda fp: (fp.o[2]==maxz) and np.abs(np.dot(fp.n, np.array([0,0,1])))>0.999)
828
+
682
829
  class GeoPoint(GeoObject):
683
830
  dim = 0
684
831
  _default_name: str = 'GeoPoint'
@@ -244,6 +244,7 @@ class FreqCoordDependent(MatProperty):
244
244
  if scalar is not None:
245
245
  def _func(f, x, y, z) -> np.ndarray:
246
246
  return np.eye(3)[:, :, None] * scalar(f,x,y,z)[None, None, :]
247
+
247
248
  if vector is not None:
248
249
  def _func(f,x, y, z) -> np.ndarray:
249
250
  N = x.shape[0]
@@ -251,6 +252,7 @@ class FreqCoordDependent(MatProperty):
251
252
  idx = np.arange(3)
252
253
  out[idx, idx, :] = vector(f,x,y,z)
253
254
  return out
255
+
254
256
  if matrix is not None:
255
257
  _func = matrix
256
258
 
emerge/_emerge/mesh3d.py CHANGED
@@ -420,7 +420,7 @@ class Mesh3D(Mesh):
420
420
  self.tet_to_edge_sign = np.zeros((6, self.tets.shape[1]), dtype=int) + _MISSING_ID
421
421
  self.tet_to_tri = np.zeros((4, self.tets.shape[1]), dtype=int) + _MISSING_ID
422
422
  self.tet_to_tri_sign = np.zeros((4, self.tets.shape[1]), dtype=int) + _MISSING_ID
423
-
423
+
424
424
  tri_to_tet = defaultdict(list)
425
425
  for itet in range(self.tets.shape[1]):
426
426
  edge_ids = [self.get_edge(self.tets[i-1,itet],self.tets[j-1,itet]) for i,j in zip([1, 1, 1, 2, 4, 3], [2, 3, 4, 3, 2, 4])]
@@ -503,7 +503,6 @@ class Mesh3D(Mesh):
503
503
  continue
504
504
  self.etag_to_edge[t] = [int(self.edge_t2i.get(tag,None)) for tag in edge_tags[0] if tag in self.edge_t2i]
505
505
 
506
-
507
506
  ## Tag bindings
508
507
  logger.trace('Constructing geometry to mesh mappings.')
509
508
  face_dimtags = gmsh.model.get_entities(2)
@@ -512,7 +511,6 @@ class Mesh3D(Mesh):
512
511
  node_tags = [self.n_t2i[int(t)] for t in node_tags[0]]
513
512
  self.ftag_to_node[t] = node_tags
514
513
  node_tags = np.squeeze(np.array(node_tags)).reshape(-1,3).T
515
-
516
514
  self.ftag_to_tri[t] = [self.get_tri(node_tags[0,i], node_tags[1,i], node_tags[2,i]) for i in range(node_tags.shape[1])]
517
515
  self.ftag_to_edge[t] = sorted(list(np.unique(self.tri_to_edge[:,self.ftag_to_tri[t]].flatten())))
518
516
 
emerge/_emerge/mesher.py CHANGED
@@ -75,9 +75,13 @@ class Mesher:
75
75
  self.objects: list[GeoObject] = []
76
76
  self.size_definitions: list[tuple[int, float]] = []
77
77
  self.mesh_fields: list[int] = []
78
+
78
79
  self._amr_fields: list[int] = []
79
80
  self._amr_coords: np.ndarray = None
80
81
  self._amr_sizes: np.ndarray = None
82
+ self._amr_ratios: np.ndrray = None
83
+ self._amr_new: np.ndarray = None
84
+
81
85
  self.min_size: float = None
82
86
  self.max_size: float = None
83
87
  self.periodic_cell: PeriodicCell = None
@@ -152,7 +156,6 @@ class Mesher:
152
156
  3: dict()}
153
157
  if len(objects) > 0: # type: ignore
154
158
  dimtags, output_mapping = gmsh.model.occ.fragment(final_dimtags, embedding_dimtags)
155
-
156
159
  for domain, mapping in zip(final_dimtags + embedding_dimtags, output_mapping):
157
160
  tag_mapping[domain[0]][domain[1]] = [o[1] for o in mapping]
158
161
  for dom in objects: # type: ignore
@@ -289,7 +292,7 @@ class Mesher:
289
292
  gmsh.model.mesh.setSizeFromBoundary(dimtag[0], dimtag[1], 0)
290
293
 
291
294
 
292
- def add_refinement_points(self, coords: np.ndarray, sizes: np.ndarray) -> None:
295
+ def add_refinement_points(self, coords: np.ndarray, sizes: np.ndarray, ratios: np.ndarray):
293
296
  if self._amr_coords is None:
294
297
  self._amr_coords = coords
295
298
  else:
@@ -299,25 +302,46 @@ class Mesher:
299
302
  self._amr_sizes = sizes
300
303
  else:
301
304
  self._amr_sizes = np.hstack((self._amr_sizes, sizes))
302
-
305
+
306
+ if self._amr_ratios is None:
307
+ self._amr_ratios = ratios
308
+ else:
309
+ self._amr_ratios = np.hstack((self._amr_ratios, ratios))
310
+
311
+ if self._amr_new is None:
312
+ self._amr_new = np.ones_like(sizes)
313
+ else:
314
+ self._amr_new = np.hstack((0.0*self._amr_new, np.ones_like(sizes)))
315
+
316
+
303
317
  def set_refinement_function(self,
304
- refinement: float,
305
318
  gr: float = 1.5,
306
319
  _qf: float = 1.0):
307
320
  xs = self._amr_coords[0,:]
308
321
  ys = self._amr_coords[1,:]
309
322
  zs = self._amr_coords[2,:]
310
- newsize = refinement*self._amr_sizes
323
+ newsize = self._amr_ratios*self._amr_sizes
311
324
  A = newsize/gr
312
325
  B = (1-gr)/gr
313
326
  from numba import njit, i8, f8
314
327
 
315
328
  @njit(f8(i8,i8,f8,f8,f8,f8), nogil=True, fastmath=True, parallel=False)
316
329
  def func(dim, tag, x, y, z, lc):
317
- sizes = np.maximum(newsize, A - B * _qf*np.sqrt((x-xs)**2 + (y-ys)**2 + (z-zs)**2))
330
+ sizes = np.maximum(newsize, A - B * _qf*np.clip(np.sqrt((x-xs)**2 + (y-ys)**2 + (z-zs)**2) - newsize/3, a_min=0, a_max=None))
318
331
  return min(lc, float(np.min(sizes)))
319
- gmsh.model.mesh.setSizeCallback(func)
320
332
 
333
+ gmsh.model.mesh.setSizeCallback(func)
334
+
335
+ def refine_finer(self, factor: float):
336
+ newids = self._amr_new==1
337
+ self._amr_ratios[newids] = self._amr_ratios[newids]*(0.85**factor)
338
+ return self._amr_ratios[newids][0]
339
+
340
+ def refine_coarser(self, factor: float):
341
+ newids = self._amr_new==1
342
+ self._amr_ratios[newids] = self._amr_ratios[newids]/(0.85**factor)
343
+ return self._amr_ratios[newids][0]
344
+
321
345
  def add_refinement_point(self,
322
346
  coordinate: np.ndarray,
323
347
  refinement: float,
@@ -389,7 +413,7 @@ class Mesher:
389
413
  if obj.dim==2:
390
414
  logger.warning('Forwarding to set_face_size')
391
415
  self.set_face_size(obj, size)
392
- logger.debug(f'Setting size {size*1000:.3f}ff for object {obj}')
416
+ logger.debug(f'Setting size {size*1000:.3f}mm for object {obj}')
393
417
  self._set_size_in_domain(obj.tags, size)
394
418
 
395
419
  def set_face_size(self, obj: GeoSurface | Selection, size: float):
@@ -405,7 +429,7 @@ class Mesher:
405
429
  logger.warning('Forwarding to set_domain_size')
406
430
  self.set_face_size(obj, size)
407
431
 
408
- logger.debug(f'Setting size {size*1000:.3f}ff for face {obj}')
432
+ logger.debug(f'Setting size {size*1000:.3f}mm for face {obj}')
409
433
  self._set_size_on_face(obj.tags, size)
410
434
 
411
435
  def set_size(self, obj: GeoObject, size: float) -> None: