Skip to content

Commit 03d43b6

Browse files
Added more surface refinement options for the GAI surface mesher (#1597)
* Added default and local option for resolving face boundaries, and option to specify curvature_resolution_angle at the face level with the GAI surface mesher * Linter fixes * Apply suggestions from code review Co-authored-by: Ben <106089368+benflexcompute@users.noreply.github.com> * Added validation check to ensure that SurfaceRefinement has at least one specified option. --------- Co-authored-by: Ben <106089368+benflexcompute@users.noreply.github.com>
1 parent 23eb7b6 commit 03d43b6

File tree

9 files changed

+153
-36
lines changed

9 files changed

+153
-36
lines changed

flow360/component/simulation/meshing_param/face_params.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212
GhostSurface,
1313
Surface,
1414
)
15-
from flow360.component.simulation.unit_system import LengthType
15+
from flow360.component.simulation.unit_system import AngleType, LengthType
1616
from flow360.component.simulation.validation.validation_context import (
1717
get_validation_info,
1818
)
1919
from flow360.component.simulation.validation.validation_utils import (
2020
check_deleted_surface_in_entity_list,
21+
check_geometry_ai_features,
2122
check_ghost_surface_usage_policy_for_face_refinements,
2223
)
2324

@@ -43,8 +44,22 @@ class SurfaceRefinement(Flow360BaseModel):
4344
alias="faces"
4445
)
4546
# pylint: disable=no-member
46-
max_edge_length: LengthType.Positive = pd.Field(
47-
description="Maximum edge length of surface cells."
47+
max_edge_length: Optional[LengthType.Positive] = pd.Field(
48+
None, description="Maximum edge length of surface cells."
49+
)
50+
51+
curvature_resolution_angle: Optional[AngleType.Positive] = pd.Field(
52+
None,
53+
description=(
54+
"Default maximum angular deviation in degrees. "
55+
"This value will restrict the angle between a cell’s normal and its underlying surface normal."
56+
),
57+
)
58+
59+
resolve_face_boundaries: Optional[bool] = pd.Field(
60+
None,
61+
description="Flag to specify whether boundaries between adjacent faces should be resolved "
62+
+ "accurately during the surface meshing process using anisotropic mesh refinement.",
4863
)
4964

5065
@pd.field_validator("entities", mode="after")
@@ -56,6 +71,28 @@ def ensure_surface_existence(cls, value):
5671
)
5772
return check_deleted_surface_in_entity_list(value)
5873

74+
@pd.field_validator("curvature_resolution_angle", "resolve_face_boundaries", mode="after")
75+
@classmethod
76+
def ensure_geometry_ai_features(cls, value, info):
77+
"""Validate that the feature is only used when Geometry AI is enabled."""
78+
return check_geometry_ai_features(cls, value, info)
79+
80+
@pd.model_validator(mode="after")
81+
def require_at_least_one_setting(self):
82+
"""Ensure that at least one of max_edge_length, curvature_resolution_angle,
83+
or resolve_face_boundaries is specified for SurfaceRefinement.
84+
"""
85+
if (
86+
self.max_edge_length is None
87+
and self.curvature_resolution_angle is None
88+
and self.resolve_face_boundaries is None
89+
):
90+
raise ValueError(
91+
"SurfaceRefinement requires at least one of 'max_edge_length', "
92+
"'curvature_resolution_angle', or 'resolve_face_boundaries' to be specified."
93+
)
94+
return self
95+
5996

6097
class GeometryRefinement(Flow360BaseModel):
6198
"""

flow360/component/simulation/meshing_param/params.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
ContextField,
3535
get_validation_info,
3636
)
37-
from flow360.component.simulation.validation.validation_utils import EntityUsageMap
37+
from flow360.component.simulation.validation.validation_utils import (
38+
EntityUsageMap,
39+
check_geometry_ai_features,
40+
)
3841

3942
RefinementTypes = Annotated[
4043
Union[
@@ -157,22 +160,32 @@ class MeshingDefaults(Flow360BaseModel):
157160
"Default maximum angular deviation in degrees. This value will restrict:"
158161
" 1. The angle between a cell’s normal and its underlying surface normal."
159162
" 2. The angle between a line segment’s normal and its underlying curve normal."
160-
" This can not be overridden per face."
163+
" This can be overridden per face only when using geometry AI."
161164
),
162165
context=SURFACE_MESH,
163166
)
164167

168+
resolve_face_boundaries: bool = pd.Field(
169+
False,
170+
description="Flag to specify whether boundaries between adjacent faces should be resolved "
171+
+ "accurately during the surface meshing process using anisotropic mesh refinement. "
172+
+ "This option is only supported when using geometry AI, and can be overridden "
173+
+ "per face with :class:`~flow360.SurfaceRefinement`.",
174+
)
175+
165176
preserve_thin_geometry: bool = pd.Field(
166177
False,
167178
description="Flag to specify whether thin geometry features with thickness roughly equal "
168-
+ "to geometry_accuracy should be resolved accurately during the surface meshing process."
169-
+ "This can be overridden with class: ~flow360.GeometryRefinement",
179+
+ "to geometry_accuracy should be resolved accurately during the surface meshing process. "
180+
+ "This option is only supported when using geometry AI, and can be overridden "
181+
+ "per face with :class:`~flow360.GeometryRefinement`.",
170182
)
171183

172184
sealing_size: LengthType.NonNegative = pd.Field(
173185
0.0 * u.m,
174186
description="Threshold size below which all geometry gaps are automatically closed. "
175-
+ "This can be overridden with class: ~flow360.GeometryRefinement",
187+
+ "This option is only supported when using geometry AI, and can be overridden "
188+
+ "per face with :class:`~flow360.GeometryRefinement`.",
176189
)
177190

178191
remove_non_manifold_faces: bool = pd.Field(
@@ -212,25 +225,16 @@ def invalid_geometry_accuracy(cls, value):
212225
@pd.field_validator(
213226
"surface_max_aspect_ratio",
214227
"surface_max_adaptation_iterations",
228+
"resolve_face_boundaries",
215229
"preserve_thin_geometry",
216230
"sealing_size",
217231
"remove_non_manifold_faces",
218232
mode="after",
219233
)
220234
@classmethod
221-
def invalid_geometry_ai_features(cls, value, info):
222-
"""Ensure GAI features are not specified when GAI is not used"""
223-
validation_info = get_validation_info()
224-
225-
if validation_info is None:
226-
return value
227-
228-
# pylint: disable=unsubscriptable-object
229-
default_value = cls.model_fields[info.field_name].default
230-
if value != default_value and not validation_info.use_geometry_AI:
231-
raise ValueError(f"{info.field_name} is only supported when geometry AI is used.")
232-
233-
return value
235+
def ensure_geometry_ai_features(cls, value, info):
236+
"""Validate that the feature is only used when Geometry AI is enabled."""
237+
return check_geometry_ai_features(cls, value, info)
234238

235239

236240
class MeshingParams(Flow360BaseModel):

flow360/component/simulation/translator/surface_meshing_translator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def _get_volume_zones(volume_zones_list: list[dict]):
165165
"curvature_resolution_angle": None,
166166
"surface_edge_growth_rate": None,
167167
"geometry_accuracy": None,
168+
"resolve_face_boundaries": None,
168169
"preserve_thin_geometry": None,
169170
"surface_max_aspect_ratio": None,
170171
"surface_max_adaptation_iterations": None,

flow360/component/simulation/validation/validation_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,18 @@ def add_entity_usage(self, entity, model_type):
366366
)
367367
entity_log["model_list"].append(model_type)
368368
self.dict_entity[entity_type][entity_key] = entity_log
369+
370+
371+
def check_geometry_ai_features(cls, value, info):
372+
"""Ensure GAI features are not specified when GAI is not used"""
373+
validation_info = get_validation_info()
374+
375+
if validation_info is None:
376+
return value
377+
378+
# pylint: disable=unsubscriptable-object
379+
default_value = cls.model_fields[info.field_name].default
380+
if value != default_value and not validation_info.use_geometry_AI:
381+
raise ValueError(f"{info.field_name} is only supported when geometry AI is used.")
382+
383+
return value

tests/ref/simulation/service_init_geometry.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"value": 12.0,
1717
"units": "degree"
1818
},
19+
"resolve_face_boundaries": false,
1920
"preserve_thin_geometry": false,
2021
"sealing_size": {
2122
"value": 0.0,

tests/ref/simulation/service_init_surface_mesh.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"value": 12.0,
1717
"units": "degree"
1818
},
19+
"resolve_face_boundaries": false,
1920
"preserve_thin_geometry": false,
2021
"sealing_size": {
2122
"value": 0.0,

tests/simulation/params/meshing_validation/test_meshing_param_validation.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import pydantic as pd
22
import pytest
33

4-
from flow360.component.simulation.meshing_param.params import MeshingParams
4+
import flow360.component.simulation.units as u
5+
from flow360.component.simulation.meshing_param.face_params import SurfaceRefinement
6+
from flow360.component.simulation.meshing_param.params import (
7+
MeshingDefaults,
8+
MeshingParams,
9+
)
510
from flow360.component.simulation.meshing_param.volume_params import (
611
AutomatedFarfield,
712
AxisymmetricRefinement,
@@ -18,6 +23,7 @@
1823
from flow360.component.simulation.simulation_params import SimulationParams
1924
from flow360.component.simulation.unit_system import CGS_unit_system
2025
from flow360.component.simulation.validation.validation_context import (
26+
SURFACE_MESH,
2127
VOLUME_MESH,
2228
ParamsValidationInfo,
2329
ValidationContext,
@@ -493,3 +499,47 @@ def test_enclosed_entities_none_does_not_raise():
493499
spacing_radial=0.2,
494500
spacing_circumferential=20,
495501
)
502+
503+
504+
def test_resolve_face_boundary_only_in_gai_mesher():
505+
# raise when GAI is off
506+
with pytest.raises(
507+
pd.ValidationError,
508+
match=r"resolve_face_boundaries is only supported when geometry AI is used",
509+
):
510+
with ValidationContext(SURFACE_MESH, non_gai_context):
511+
with CGS_unit_system:
512+
MeshingParams(
513+
defaults=MeshingDefaults(
514+
boundary_layer_first_layer_thickness=0.1, resolve_face_boundaries=True
515+
)
516+
)
517+
518+
519+
def test_surface_refinement_in_gai_mesher():
520+
# raise when GAI is off
521+
with pytest.raises(
522+
pd.ValidationError,
523+
match=r"curvature_resolution_angle is only supported when geometry AI is used",
524+
):
525+
with ValidationContext(SURFACE_MESH, non_gai_context):
526+
with CGS_unit_system:
527+
SurfaceRefinement(max_edge_length=0.1, curvature_resolution_angle=10 * u.deg)
528+
529+
# raise when GAI is off
530+
with pytest.raises(
531+
pd.ValidationError,
532+
match=r"resolve_face_boundaries is only supported when geometry AI is used",
533+
):
534+
with ValidationContext(SURFACE_MESH, non_gai_context):
535+
with CGS_unit_system:
536+
SurfaceRefinement(resolve_face_boundaries=True)
537+
538+
# raise when no options are specified
539+
with pytest.raises(
540+
pd.ValidationError,
541+
match=r"SurfaceRefinement requires at least one of 'max_edge_length', 'curvature_resolution_angle', or 'resolve_face_boundaries' to be specified",
542+
):
543+
with ValidationContext(SURFACE_MESH, non_gai_context):
544+
with CGS_unit_system:
545+
SurfaceRefinement(entities=Surface(name="testFace"))

tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"value": 0.05,
1515
"units": "1.0*m"
1616
},
17+
"resolve_face_boundaries": false,
1718
"preserve_thin_geometry": false,
1819
"surface_max_aspect_ratio": 0.01,
1920
"surface_max_adaptation_iterations": 19,
@@ -151,7 +152,12 @@
151152
"max_edge_length": {
152153
"value": 0.1,
153154
"units": "1.0*m"
154-
}
155+
},
156+
"curvature_resolution_angle": {
157+
"value": 0.08726646259971647,
158+
"units": "rad"
159+
},
160+
"resolve_face_boundaries": true
155161
},
156162
{
157163
"name": "Local_override",

tests/simulation/translator/test_surface_meshing_translator.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,11 @@ def airplane_surface_mesh():
382382

383383
@pytest.fixture()
384384
def rotor_surface_mesh():
385-
rotor_geopmetry = TempGeometry("rotor.csm")
385+
rotor_geometry = TempGeometry("rotor.csm")
386386
with imperial_unit_system:
387387
param = SimulationParams(
388388
private_attribute_asset_cache=AssetCache(
389-
project_entity_info=rotor_geopmetry._get_entity_info()
389+
project_entity_info=rotor_geometry._get_entity_info()
390390
),
391391
meshing=MeshingParams(
392392
defaults=MeshingDefaults(
@@ -396,40 +396,40 @@ def rotor_surface_mesh():
396396
),
397397
refinements=[
398398
SurfaceRefinement(
399-
entities=[rotor_geopmetry["body01_face003"]],
399+
entities=[rotor_geometry["body01_face003"]],
400400
max_edge_length=0.1 * u.inch,
401401
),
402402
SurfaceRefinement(
403403
entities=[
404-
rotor_geopmetry["body01_face001"],
405-
rotor_geopmetry["body01_face002"],
404+
rotor_geometry["body01_face001"],
405+
rotor_geometry["body01_face002"],
406406
],
407407
max_edge_length=10 * u.inch,
408408
),
409409
SurfaceEdgeRefinement(
410-
entities=[rotor_geopmetry["body01_edge001"]],
410+
entities=[rotor_geometry["body01_edge001"]],
411411
method=AngleBasedRefinement(value=1 * u.degree),
412412
),
413413
SurfaceEdgeRefinement(
414414
entities=[
415-
rotor_geopmetry["body01_edge002"],
416-
rotor_geopmetry["body01_edge003"],
415+
rotor_geometry["body01_edge002"],
416+
rotor_geometry["body01_edge003"],
417417
],
418418
method=HeightBasedRefinement(value=0.05 * u.inch),
419419
),
420420
SurfaceEdgeRefinement(
421421
entities=[
422-
rotor_geopmetry["body01_edge004"],
423-
rotor_geopmetry["body01_edge006"],
422+
rotor_geometry["body01_edge004"],
423+
rotor_geometry["body01_edge006"],
424424
],
425425
method=ProjectAnisoSpacing(),
426426
),
427427
SurfaceEdgeRefinement(
428-
entities=[rotor_geopmetry["body01_edge005"]],
428+
entities=[rotor_geometry["body01_edge005"]],
429429
method=HeightBasedRefinement(value=0.01 * u.inch),
430430
),
431431
SurfaceEdgeRefinement(
432-
entities=[rotor_geopmetry["body01_edge007"]],
432+
entities=[rotor_geometry["body01_edge007"]],
433433
method=HeightBasedRefinement(value=0.01 * u.inch),
434434
),
435435
],
@@ -531,7 +531,7 @@ def test_gai_surface_mesher_refinements():
531531
params = SimulationParams(
532532
meshing=MeshingParams(
533533
defaults=MeshingDefaults(
534-
geometry_accuracy=0.05 * u.m, # GAI setting
534+
geometry_accuracy=0.05 * u.m, # GAI only setting
535535
surface_max_edge_length=0.2,
536536
boundary_layer_first_layer_thickness=0.01,
537537
surface_max_aspect_ratio=0.01,
@@ -543,6 +543,8 @@ def test_gai_surface_mesher_refinements():
543543
name="renamed_surface",
544544
max_edge_length=0.1,
545545
faces=[geometry["*"]],
546+
curvature_resolution_angle=5.0 * u.deg, # GAI only setting
547+
resolve_face_boundaries=True, # GAI only setting
546548
),
547549
GeometryRefinement(
548550
name="Local_override",

0 commit comments

Comments
 (0)