Skip to content

Commit 04e836e

Browse files
committed
Merge branch 'main' of github.com:simbilod/meshwell
2 parents a11fae3 + 86421ff commit 04e836e

File tree

11 files changed

+11405
-8832
lines changed

11 files changed

+11405
-8832
lines changed

meshwell/labeledentity.py

Lines changed: 49 additions & 271 deletions
Large diffs are not rendered by default.

meshwell/model.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,6 @@ def mesh(
482482
refinement_field_indices,
483483
refinement_max_index,
484484
default_characteristic_length,
485-
final_entity_list,
486485
)
487486

488487
# Use the smallest element size overall

meshwell/resolution.py

Lines changed: 174 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,192 @@
11
import numpy as np
22
import copy
3+
from typing import Literal, Any
34
from pydantic import BaseModel
45

56

67
class ResolutionSpec(BaseModel):
8+
"""A ResolutionSpec is attached to a pre-CAD entity.
9+
10+
It sets a mesh size field (see child classes) to the resulting post-CAD volumes, surfaces, curves, or points.
11+
12+
The volumes, surfaces, curves can be filtered based on their mass (volume, area, length). Points can be filtered based on the length of the curve they belong to.
713
"""
8-
Object holding resolution information for an entity and its boundaries.
914

10-
# FIXME: make a better ResolutionSpec class that handles the entity/boundary distinction
15+
# Eventually we can add flags here to also consider proximity to other specific physicals (e.g. shared interfaces)
16+
apply_to: Literal["volumes", "surfaces", "curves", "points"]
17+
min_mass: float = 0
18+
max_mass: float = np.inf
1119

12-
Arguments:
13-
# Volume
14-
volume_resolution (float): resolution of the volume (3D). No effect if the entity is 2D.
15-
defaults to inf --> default resolution, since the local resolution is always the minimum of all size fields
20+
@property
21+
def entity_str(self):
22+
"""Convenience wrapper."""
23+
if self.apply_to == "volumes":
24+
return "RegionsList"
25+
elif self.apply_to == "surfaces":
26+
return "SurfacesList"
27+
elif self.apply_to == "curves":
28+
return "CurvesList"
29+
elif self.apply_to == "points":
30+
return "PointsList"
1631

17-
# Surface
18-
surface_resolution (float): resolution of the surface (2D) or of all the surfaces touching the volume (3D)
19-
defaults to inf --> default resolution (2D) or volume_resolution (3D), since the local resolution is always the minimum of all size fields
32+
@property
33+
def target_dimension(self):
34+
"""Convenience wrapper."""
35+
if self.apply_to == "volumes":
36+
return 3
37+
elif self.apply_to == "surfaces":
38+
return 2
39+
elif self.apply_to == "curves":
40+
return 1
41+
elif self.apply_to == "points":
42+
return 0
2043

21-
# Curves
22-
curve_resolution (float): resolution of curves constituting the volumes' surfaces (3D) or surfaces (2D)
23-
defaults to inf --> surface_resolution, since the local resolution is always the minimum of all size fields
2444

25-
# Points
26-
point_resolution (float): resolution of points constituting the volumes' surfaces' curves (3D) or surfaces' curves (2D)
27-
defaults to inf --> curve_resolution, since the local resolution is always the minimum of all size fields
28-
can be filtered by the length of the associated curves
29-
"""
45+
class ConstantInField(ResolutionSpec):
46+
"""Constant resolution within the entities."""
47+
48+
resolution: float
49+
50+
def apply(
51+
self,
52+
model: Any,
53+
current_field_index: int,
54+
entities_mass_dict,
55+
refinement_field_indices,
56+
) -> int:
57+
new_field_indices = []
3058

31-
# Volume
32-
resolution_volumes: float | None = None
33-
min_volumes: float = 0
34-
max_volumes: float = np.inf
35-
# Surface
36-
resolution_surfaces: float | None = None
37-
min_area_surfaces: float = 0
38-
max_area_surfaces: float = np.inf
39-
distmax_surfaces: float | None = None
40-
sizemax_surfaces: float | None = None
41-
surface_sigmoid: bool = False
42-
surface_per_sampling_surfaces: float | None = None
43-
sampling_surface_max: int = 100
44-
# Curve
45-
resolution_curves: float | None = None
46-
min_length_curves: float = 0
47-
max_length_curves: float = np.inf
48-
distmax_curves: float | None = None
49-
sizemax_curves: float | None = None
50-
curve_sigmoid: bool = False
51-
length_per_sampling_curves: float | None = None
52-
sampling_curve_max: int = 100
53-
# Point
54-
resolution_points: float | None = None
55-
min_length_curves_for_points: float = 0
56-
max_length_curves_for_points: float = np.inf
57-
distmax_points: float | None = None
58-
sizemax_points: float | None = None
59-
point_sigmoid: bool = False
59+
model.mesh.field.add("MathEval", current_field_index)
60+
model.mesh.field.setString(current_field_index, "F", f"{self.resolution}")
61+
model.mesh.field.add("Restrict", current_field_index + 1)
62+
model.mesh.field.setNumber(
63+
current_field_index + 1, "InField", current_field_index
64+
)
65+
model.mesh.field.setNumbers(
66+
current_field_index + 1,
67+
self.entity_str,
68+
list(entities_mass_dict.keys()),
69+
)
70+
new_field_indices = (current_field_index + 1,)
71+
current_field_index += 2
72+
73+
return new_field_indices, current_field_index
6074

6175
def refine(self, resolution_factor: float):
6276
result = copy.copy(self)
63-
if result.resolution_volumes is not None:
64-
result.resolution_volumes *= resolution_factor
65-
if result.resolution_surfaces is not None:
66-
result.resolution_surfaces *= resolution_factor
67-
if result.sizemax_surfaces is not None:
68-
result.sizemax_surfaces *= resolution_factor
69-
if result.resolution_curves is not None:
70-
result.resolution_curves *= resolution_factor
71-
if result.sizemax_curves is not None:
72-
result.sizemax_curves *= resolution_factor
73-
if result.resolution_points is not None:
74-
result.resolution_points *= resolution_factor
75-
if result.sizemax_points is not None:
76-
result.sizemax_points *= resolution_factor
77+
if result.resolution is not None:
78+
result.resolution *= resolution_factor
79+
7780
return result
7881

79-
def calculate_sampling(self, mass_per_sampling, mass, max_sampling):
80-
if mass_per_sampling is None:
81-
return 2 # avoid int(inf) error
82-
else:
83-
return min(max(2, int(mass / mass_per_sampling)), max_sampling)
84-
85-
def calculate_sampling_surface(self, area):
86-
if self.surface_per_sampling_surfaces:
87-
return self.calculate_sampling(
88-
self.surface_per_sampling_surfaces, area, self.sampling_surface_max
89-
)
90-
else:
91-
return self.calculate_sampling(
92-
0.5 * self.resolution_surfaces, area, self.sampling_surface_max
93-
)
94-
95-
def calculate_sampling_curve(self, length):
96-
if self.length_per_sampling_curves:
97-
return self.calculate_sampling(
98-
self.length_per_sampling_curves, length, self.sampling_curve_max
99-
)
100-
else:
101-
return self.calculate_sampling(
102-
0.5 * self.resolution_curves, length, self.sampling_curve_max
103-
)
82+
83+
class SampledField(ResolutionSpec):
84+
"""Shared functionality for size fields that require sampling the entities at points."""
85+
86+
mass_per_sampling: float | None = None
87+
max_sampling: int = 100
88+
sizemin: float
89+
90+
def calculate_samplings(self, entities_mass_dict):
91+
"""Calculates a more optimal sampling from the masses"""
92+
93+
if self.mass_per_sampling is None:
94+
# Default sampling is half the minimum resolution
95+
mass_per_sampling = 0.5 * self.sizemin
96+
97+
return {
98+
tag: min(max(2, int(mass / mass_per_sampling)), self.max_sampling)
99+
for tag, mass in entities_mass_dict.items()
100+
}
101+
102+
def refine(self, resolution_factor: float):
103+
result = copy.copy(self)
104+
if result.sizemax is not None:
105+
result.sizemax *= resolution_factor
106+
if result.sizemin is not None:
107+
result.sizemin *= resolution_factor
108+
109+
110+
class ThresholdField(SampledField):
111+
"""Linear growth of the resolution away from the entity"""
112+
113+
sizemax: float
114+
sizemin: float
115+
distmin: float = 0
116+
distmax: float
117+
118+
def apply(
119+
self,
120+
model: Any,
121+
current_field_index: int,
122+
entities_mass_dict,
123+
refinement_field_indices,
124+
) -> int:
125+
new_field_indices = []
126+
127+
# Compute samplings
128+
samplings_dict = self.calculate_samplings(entities_mass_dict)
129+
130+
# FIXME: It is computationally cheaper to have a large sampling on all the curves rather than one field per curve; but there is probably an optimum somewhere.
131+
# FOr instance, the distribution should be very skewed (tiny vertical curves, tiny curves in bends, vs long horizontal ones), so there may be benefits for a small number of optimized fields.
132+
samplings = max(samplings_dict.values())
133+
entities = list(entities_mass_dict.keys())
134+
135+
model.mesh.field.add("Distance", current_field_index)
136+
model.mesh.field.setNumbers(current_field_index, self.entity_str, entities)
137+
model.mesh.field.setNumber(current_field_index, "Sampling", samplings)
138+
model.mesh.field.add("Threshold", current_field_index + 1)
139+
model.mesh.field.setNumber(
140+
current_field_index + 1, "InField", current_field_index
141+
)
142+
model.mesh.field.setNumber(current_field_index + 1, "SizeMin", self.sizemin)
143+
model.mesh.field.setNumber(current_field_index + 1, "DistMin", self.distmin)
144+
if self.sizemax and self.distmax:
145+
model.mesh.field.setNumber(current_field_index + 1, "SizeMax", self.sizemax)
146+
model.mesh.field.setNumber(current_field_index + 1, "DistMax", self.distmax)
147+
model.mesh.field.setNumber(current_field_index + 1, "StopAtDistMax", 1)
148+
new_field_indices = (current_field_index + 1,)
149+
current_field_index += 2
150+
151+
return new_field_indices, current_field_index
152+
153+
154+
class ExponentialField(SampledField):
155+
"""Exponential growth of the characteristic length away from the entity"""
156+
157+
growth_factor: float
158+
159+
def apply(
160+
self,
161+
model: Any,
162+
current_field_index: int,
163+
entities_mass_dict,
164+
refinement_field_indices,
165+
) -> int:
166+
new_field_indices = []
167+
168+
# Compute samplings
169+
samplings_dict = self.calculate_samplings(entities_mass_dict)
170+
171+
# FIXME: It is computationally cheaper to have a large sampling on all the curves rather than one field per curve; but there is probably an optimum somewhere.
172+
# FOr instance, the distribution should be very skewed (tiny vertical curves, tiny curves in bends, vs long horizontal ones), so there may be benefits for a small number of optimized fields.
173+
samplings = max(samplings_dict.values())
174+
entities = list(entities_mass_dict.keys())
175+
176+
# Sampled distance field
177+
model.mesh.field.add("Distance", current_field_index)
178+
model.mesh.field.setNumbers(current_field_index, self.entity_str, entities)
179+
model.mesh.field.setNumber(current_field_index, "Sampling", samplings)
180+
181+
# Math field
182+
model.mesh.field.add("MathEval", current_field_index + 1)
183+
model.mesh.field.setString(
184+
current_field_index + 1,
185+
"F",
186+
f"({self.growth_factor}^F{current_field_index} - 1) + {self.sizemin}",
187+
)
188+
189+
new_field_indices = (current_field_index + 1,)
190+
current_field_index += 2
191+
192+
return new_field_indices, current_field_index

meshwell/tag.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ def tag_entities(entity_list: List):
1313
3: {},
1414
}
1515
for entities in entity_list:
16-
dim = entities.get_dim()
16+
dim = entities.dim
1717
for physical_name in entities.physical_name:
1818
if physical_name not in names_to_tags[dim]:
1919
names_to_tags[dim][physical_name] = []
20-
names_to_tags[dim][physical_name].extend(entities.get_tags())
20+
names_to_tags[dim][physical_name].extend(entities.tags)
2121

2222
for dim in names_to_tags.keys():
2323
for physical_name, tags in names_to_tags[dim].items():
@@ -34,12 +34,12 @@ def tag_interfaces(entity_list: List, max_dim: int, boundary_delimiter: str):
3434
for entity1, entity2 in combinations(entity_list, 2):
3535
if entity1.physical_name == entity2.physical_name:
3636
continue
37-
elif entity1.get_dim() != entity2.get_dim():
37+
elif entity1.dim != entity2.dim:
3838
continue
39-
elif entity1.get_dim() != max_dim:
39+
elif entity1.dim != max_dim:
4040
continue
4141
else:
42-
dim = entity1.get_dim() - 1
42+
dim = entity1.dim - 1
4343
common_interfaces = list(
4444
set(entity1.boundaries).intersection(entity2.boundaries)
4545
)
@@ -73,9 +73,9 @@ def tag_boundaries(
7373
2: {},
7474
}
7575
for entity in entity_list:
76-
if entity.get_dim() != max_dim:
76+
if entity.dim != max_dim:
7777
continue
78-
dim = entity.get_dim() - 1
78+
dim = entity.dim - 1
7979
boundaries = list(set(entity.boundaries) - set(entity.interfaces))
8080
for entity_physical_name in entity.physical_name:
8181
boundary_name = (

0 commit comments

Comments
 (0)