Skip to content

Commit fa15943

Browse files
[FXC-6102] fix(): avoid false-positive deleted-surface validation for auto farfield full-body and add regression tests (#1871)
1 parent 5717246 commit fa15943

File tree

2 files changed

+155
-6
lines changed

2 files changed

+155
-6
lines changed

flow360/component/simulation/primitives.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,26 @@ def _check_axis_is_orthogonal(axis_pair: Tuple[Axis, Axis]) -> Tuple[Axis, Axis]
8080
return (tuple(axis_1), tuple(axis_2))
8181

8282

83+
def _auto_symmetric_plane_exists_from_bbox(
84+
*,
85+
global_bounding_box: BoundingBoxType,
86+
planar_face_tolerance: float,
87+
) -> bool:
88+
"""
89+
Determine whether automated farfield logic will generate a `symmetric` plane
90+
from global bounding box extents and planar-face tolerance.
91+
"""
92+
93+
y_min = global_bounding_box[0][1]
94+
y_max = global_bounding_box[1][1]
95+
tolerance = global_bounding_box.largest_dimension * planar_face_tolerance
96+
97+
positive_half = abs(y_min) < tolerance < y_max
98+
negative_half = abs(y_max) < tolerance and y_min < -tolerance
99+
100+
return positive_half or negative_half
101+
102+
83103
OrthogonalAxes = Annotated[Tuple[Axis, Axis], pd.AfterValidator(_check_axis_is_orthogonal)]
84104

85105

@@ -754,6 +774,13 @@ def _will_be_deleted_by_mesher(
754774
if half_model_symmetry_plane_center_y is None:
755775
# Legacy schema.
756776
return False
777+
if farfield_domain_type not in ("half_body_positive_y", "half_body_negative_y") and (
778+
not _auto_symmetric_plane_exists_from_bbox(
779+
global_bounding_box=global_bounding_box,
780+
planar_face_tolerance=planar_face_tolerance,
781+
)
782+
):
783+
return False
757784
return self._overlaps(half_model_symmetry_plane_center_y, length_tolerance)
758785

759786
if farfield_method in ("quasi-3d", "quasi-3d-periodic"):
@@ -874,12 +901,10 @@ def exists(self, validation_info) -> bool:
874901
if validation_info.will_generate_forced_symmetry_plane():
875902
return True
876903

877-
y_min, y_max, tolerance, _ = self._get_existence_dependency(validation_info)
878-
879-
positive_half = abs(y_min) < tolerance < y_max
880-
negative_half = abs(y_max) < tolerance and y_min < -tolerance
881-
882-
return positive_half or negative_half
904+
return _auto_symmetric_plane_exists_from_bbox(
905+
global_bounding_box=validation_info.global_bounding_box,
906+
planar_face_tolerance=validation_info.planar_face_tolerance,
907+
)
883908

884909
def _per_entity_type_validation(self, param_info: ParamsValidationInfo):
885910
"""Validate ghost surface existence and configuration."""

tests/simulation/params/test_validators_params.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2914,6 +2914,130 @@ def test_seedpoint_zone_based_params():
29142914
assert errors is None
29152915

29162916

2917+
def test_auto_farfield_full_body_surface_on_y0_not_marked_deleted():
2918+
surface_on_plane = Surface(
2919+
name="plane_surf",
2920+
private_attributes=SurfacePrivateAttributes(bounding_box=[[0, 0, 0], [1, 0, 1]]),
2921+
)
2922+
2923+
asset_cache = AssetCache(
2924+
project_length_unit="m",
2925+
use_inhouse_mesher=True,
2926+
use_geometry_AI=True,
2927+
project_entity_info=SurfaceMeshEntityInfo(
2928+
global_bounding_box=[[0, -2, 0], [1, 2, 1]], # Full-body (crosses Y=0)
2929+
boundaries=[surface_on_plane],
2930+
ghost_entities=[
2931+
GhostSphere(
2932+
name="farfield",
2933+
private_attribute_id="farfield",
2934+
center=[0, 0, 0],
2935+
max_radius=10,
2936+
),
2937+
GhostCircularPlane(
2938+
name="symmetric",
2939+
private_attribute_id="symmetric",
2940+
center=[0, 0, 0],
2941+
normal_axis=[0, 1, 0],
2942+
max_radius=10,
2943+
),
2944+
],
2945+
),
2946+
)
2947+
2948+
farfield = AutomatedFarfield()
2949+
2950+
with SI_unit_system:
2951+
params = SimulationParams(
2952+
meshing=MeshingParams(
2953+
defaults=MeshingDefaults(
2954+
planar_face_tolerance=1e-6,
2955+
geometry_accuracy=1e-5,
2956+
boundary_layer_first_layer_thickness=1e-3,
2957+
),
2958+
volume_zones=[farfield],
2959+
),
2960+
models=[
2961+
Fluid(),
2962+
Wall(entities=[surface_on_plane]),
2963+
Freestream(entities=[farfield.farfield]),
2964+
],
2965+
private_attribute_asset_cache=asset_cache,
2966+
)
2967+
2968+
_, errors, _ = validate_model(
2969+
params_as_dict=params.model_dump(mode="json"),
2970+
validated_by=ValidationCalledBy.LOCAL,
2971+
root_item_type="SurfaceMesh",
2972+
validation_level="All",
2973+
)
2974+
2975+
assert errors is None
2976+
2977+
2978+
def test_auto_farfield_half_body_surface_on_y0_marked_deleted():
2979+
surface_on_plane = Surface(
2980+
name="plane_surf",
2981+
private_attributes=SurfacePrivateAttributes(bounding_box=[[0, 0, 0], [1, 0, 1]]),
2982+
)
2983+
2984+
asset_cache = AssetCache(
2985+
project_length_unit="m",
2986+
use_inhouse_mesher=True,
2987+
use_geometry_AI=True,
2988+
project_entity_info=SurfaceMeshEntityInfo(
2989+
global_bounding_box=[[0, -2, 0], [1, 2, 1]],
2990+
boundaries=[surface_on_plane],
2991+
ghost_entities=[
2992+
GhostSphere(
2993+
name="farfield",
2994+
private_attribute_id="farfield",
2995+
center=[0, 0, 0],
2996+
max_radius=10,
2997+
),
2998+
GhostCircularPlane(
2999+
name="symmetric",
3000+
private_attribute_id="symmetric",
3001+
center=[0, 0, 0],
3002+
normal_axis=[0, 1, 0],
3003+
max_radius=10,
3004+
),
3005+
],
3006+
),
3007+
)
3008+
3009+
farfield = AutomatedFarfield(domain_type="half_body_positive_y")
3010+
3011+
with SI_unit_system:
3012+
params = SimulationParams(
3013+
meshing=MeshingParams(
3014+
defaults=MeshingDefaults(
3015+
planar_face_tolerance=1e-6,
3016+
geometry_accuracy=1e-5,
3017+
boundary_layer_first_layer_thickness=1e-3,
3018+
),
3019+
volume_zones=[farfield],
3020+
),
3021+
models=[
3022+
Fluid(),
3023+
Wall(entities=[surface_on_plane]),
3024+
Freestream(entities=[farfield.farfield]),
3025+
],
3026+
private_attribute_asset_cache=asset_cache,
3027+
)
3028+
3029+
_, errors, _ = validate_model(
3030+
params_as_dict=params.model_dump(mode="json"),
3031+
validated_by=ValidationCalledBy.LOCAL,
3032+
root_item_type="SurfaceMesh",
3033+
validation_level="All",
3034+
)
3035+
3036+
assert len(errors) == 1
3037+
assert errors[0]["loc"] == ("models", 1, "entities")
3038+
assert "Boundary `plane_surf` will likely be deleted after mesh generation." in errors[0]["msg"]
3039+
3040+
29173041
def test_deleted_surfaces_domain_type():
29183042
# Mock Asset Cache
29193043
surface_pos = Surface(

0 commit comments

Comments
 (0)