|
1 | 1 | import numpy as np |
2 | 2 | import copy |
| 3 | +from typing import Literal, Any |
3 | 4 | from pydantic import BaseModel |
4 | 5 |
|
5 | 6 |
|
6 | 7 | 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. |
7 | 13 | """ |
8 | | - Object holding resolution information for an entity and its boundaries. |
9 | 14 |
|
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 |
11 | 19 |
|
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" |
16 | 31 |
|
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 |
20 | 43 |
|
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 |
24 | 44 |
|
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 = [] |
30 | 58 |
|
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 |
60 | 74 |
|
61 | 75 | def refine(self, resolution_factor: float): |
62 | 76 | 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 | + |
77 | 80 | return result |
78 | 81 |
|
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 |
0 commit comments