Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions flow360/component/simulation/meshing_param/volume_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,57 @@ def _validate_only_in_beta_mesher(cls, value):
"`domain_type` is only supported when using both GAI surface mesher and beta volume mesher."
)

@pd.field_validator("domain_type", mode="after")
@classmethod
def _validate_domain_type_bbox(cls, value):
"""
Ensure that when domain_type is used, the model actually spans across Y=0.
"""
validation_info = get_validation_info()
if validation_info is None:
return value

if (
value not in ("half_body_positive_y", "half_body_negative_y")
or validation_info.global_bounding_box is None
):
return value

y_min = validation_info.global_bounding_box[0][1]
y_max = validation_info.global_bounding_box[1][1]

largest_dimension = -float("inf")
for dim in range(3):
dimension = (
validation_info.global_bounding_box[1][dim]
- validation_info.global_bounding_box[0][dim]
)
largest_dimension = max(largest_dimension, dimension)

tolerance = largest_dimension * validation_info.planar_face_tolerance

# Check if model crosses Y=0
crossing = y_min < -tolerance and y_max > tolerance
if crossing:
return value

# If not crossing, check if it matches the requested domain
if value == "half_body_positive_y":
# Should be on positive side (y > 0)
if y_min >= -tolerance:
return value

if value == "half_body_negative_y":
# Should be on negative side (y < 0)
if y_max <= tolerance:
return value

raise ValueError(
f"The model does not cross the symmetry plane (Y=0) with tolerance {tolerance:.2g}. "
f"Model Y range: [{y_min:.2g}, {y_max:.2g}]. "
"Please check if `domain_type` is set correctly."
)


class AutomatedFarfield(_FarfieldBase):
"""
Expand Down
19 changes: 16 additions & 3 deletions flow360/component/simulation/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,14 +554,15 @@ def _overlaps(self, ghost_surface_center_y: Optional[float], length_tolerance: f
return True

def _will_be_deleted_by_mesher(
# pylint: disable=too-many-arguments, too-many-return-statements
# pylint: disable=too-many-arguments, too-many-return-statements, too-many-branches
self,
at_least_one_body_transformed: bool,
farfield_method: Optional[Literal["auto", "quasi-3d", "quasi-3d-periodic", "user-defined"]],
global_bounding_box: Optional[BoundingBoxType],
planar_face_tolerance: Optional[float],
half_model_symmetry_plane_center_y: Optional[float],
quasi_3d_symmetry_planes_center_y: Optional[tuple[float]],
farfield_domain_type: Optional[str] = None,
) -> bool:
"""
Check against the automated farfield method and
Expand All @@ -576,12 +577,24 @@ def _will_be_deleted_by_mesher(
# VolumeMesh or Geometry/SurfaceMesh with legacy schema.
return False

length_tolerance = global_bounding_box.largest_dimension * planar_face_tolerance

if farfield_domain_type in ("half_body_positive_y", "half_body_negative_y"):
if self.private_attributes is not None:
# pylint: disable=no-member
y_min = self.private_attributes.bounding_box.ymin
y_max = self.private_attributes.bounding_box.ymax

if farfield_domain_type == "half_body_positive_y" and y_max < -length_tolerance:
return True

if farfield_domain_type == "half_body_negative_y" and y_min > length_tolerance:
return True

if farfield_method == "user-defined":
# Not applicable to user defined farfield
return False

length_tolerance = global_bounding_box.largest_dimension * planar_face_tolerance

if farfield_method == "auto":
if half_model_symmetry_plane_center_y is None:
# Legacy schema.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def ensure_output_surface_existence(cls, value):
planar_face_tolerance=validation_info.planar_face_tolerance,
half_model_symmetry_plane_center_y=validation_info.half_model_symmetry_plane_center_y,
quasi_3d_symmetry_planes_center_y=validation_info.quasi_3d_symmetry_planes_center_y,
farfield_domain_type=validation_info.farfield_domain_type,
):
raise ValueError(
f"Boundary `{value.name}` will likely be deleted after mesh generation. Therefore it cannot be used."
Expand Down
8 changes: 8 additions & 0 deletions flow360/component/simulation/validation/validation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def check_deleted_surface_in_entity_list(value):
planar_face_tolerance=validation_info.planar_face_tolerance,
half_model_symmetry_plane_center_y=validation_info.half_model_symmetry_plane_center_y,
quasi_3d_symmetry_planes_center_y=validation_info.quasi_3d_symmetry_planes_center_y,
farfield_domain_type=validation_info.farfield_domain_type,
):
raise ValueError(
f"Boundary `{surface.name}` will likely be deleted after mesh generation. "
Expand Down Expand Up @@ -115,6 +116,7 @@ def check_deleted_surface_pair(value):
planar_face_tolerance=validation_info.planar_face_tolerance,
half_model_symmetry_plane_center_y=validation_info.half_model_symmetry_plane_center_y,
quasi_3d_symmetry_planes_center_y=validation_info.quasi_3d_symmetry_planes_center_y,
farfield_domain_type=validation_info.farfield_domain_type,
):
raise ValueError(
f"Boundary `{surface.name}` will likely be deleted after mesh generation. "
Expand Down Expand Up @@ -173,6 +175,12 @@ def check_symmetric_boundary_existence(stored_entities):
if item.private_attribute_entity_type_name != "GhostCircularPlane":
continue

if validation_info.farfield_domain_type in (
"half_body_positive_y",
"half_body_negative_y",
):
continue

if not item.exists(validation_info):
# pylint: disable=protected-access
y_min, y_max, tolerance, largest_dimension = item._get_existence_dependency(
Expand Down
80 changes: 80 additions & 0 deletions tests/simulation/params/test_automated_farfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from flow360.component.project_utils import set_up_params_for_uploading
from flow360.component.resource_base import local_metadata_builder
from flow360.component.simulation import services
from flow360.component.simulation.entity_info import SurfaceMeshEntityInfo
from flow360.component.simulation.framework.param_utils import AssetCache
from flow360.component.simulation.meshing_param.face_params import SurfaceRefinement
from flow360.component.simulation.meshing_param.params import (
MeshingDefaults,
Expand Down Expand Up @@ -481,3 +483,81 @@ def _test_and_show_errors(geometry):
assert errors_1 is None
assert errors_2 is None
assert errors_3 is None


def test_domain_type_bounding_box_check():
# Case 1: Model does not cross Y=0 (Positive Half)
# y range [1, 10]
# Request half_body_positive_y -> Should pass (aligned)

dummy_boundary = Surface(name="dummy")

asset_cache_positive = AssetCache(
project_length_unit="m",
use_inhouse_mesher=True,
use_geometry_AI=True,
project_entity_info=SurfaceMeshEntityInfo(
global_bounding_box=[[0, 1, 0], [10, 10, 10]],
ghost_entities=[],
boundaries=[dummy_boundary],
),
)

farfield_pos = UserDefinedFarfield(domain_type="half_body_positive_y")

with SI_unit_system:
params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
planar_face_tolerance=0.01,
geometry_accuracy=1e-5,
boundary_layer_first_layer_thickness=1e-3,
),
volume_zones=[farfield_pos],
),
models=[Wall(entities=[dummy_boundary])], # Assign BC to avoid missing BC error
private_attribute_asset_cache=asset_cache_positive,
)

params_dict = params.model_dump(mode="json", exclude_none=True)
_, errors, _ = services.validate_model(
params_as_dict=params_dict,
validated_by=services.ValidationCalledBy.LOCAL,
root_item_type="SurfaceMesh",
validation_level="All",
)

domain_errors = [
e for e in (errors or []) if "The model does not cross the symmetry plane" in e["msg"]
]
assert len(domain_errors) == 0

# Case 2: Misaligned
# Request half_body_negative_y on Positive Model -> Should Fail
farfield_neg = UserDefinedFarfield(domain_type="half_body_negative_y")

with SI_unit_system:
params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
planar_face_tolerance=0.01,
geometry_accuracy=1e-5,
boundary_layer_first_layer_thickness=1e-3,
),
volume_zones=[farfield_neg],
),
models=[Wall(entities=[dummy_boundary])],
private_attribute_asset_cache=asset_cache_positive,
)

params_dict = params.model_dump(mode="json", exclude_none=True)
_, errors, _ = services.validate_model(
params_as_dict=params_dict,
validated_by=services.ValidationCalledBy.LOCAL,
root_item_type="SurfaceMesh",
validation_level="All",
)

assert errors is not None
domain_errors = [e for e in errors if "The model does not cross the symmetry plane" in e["msg"]]
assert len(domain_errors) == 1
90 changes: 90 additions & 0 deletions tests/simulation/params/test_validators_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2279,3 +2279,93 @@ def test_ghost_surface_pair_requires_quasi_3d_periodic_farfield():
# Case 4: Farfield method IS "quasi-3d-periodic" → should pass
with SI_unit_system, ValidationContext(CASE, quasi_3d_periodic_farfield_context):
Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational())


def test_deleted_surfaces_domain_type():
# Mock Asset Cache
surface_pos = Surface(
name="pos_surf",
private_attributes=SurfacePrivateAttributes(bounding_box=[[0, 1, 0], [1, 2, 1]]),
)
surface_neg = Surface(
name="neg_surf",
private_attributes=SurfacePrivateAttributes(bounding_box=[[0, -2, 0], [1, -1, 1]]),
)
surface_cross = Surface(
name="cross_surf",
private_attributes=SurfacePrivateAttributes(
bounding_box=[[0, -0.000001, 0], [1, 0.000001, 1]]
),
)

asset_cache = AssetCache(
project_length_unit="m",
use_inhouse_mesher=True,
use_geometry_AI=True,
project_entity_info=SurfaceMeshEntityInfo(
global_bounding_box=[[0, -2, 0], [1, 2, 1]], # Crosses Y=0
boundaries=[surface_pos, surface_neg, surface_cross],
),
)

# Test half_body_positive_y -> keeps positive, deletes negative
farfield = UserDefinedFarfield(domain_type="half_body_positive_y")

with SI_unit_system:
params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
planar_face_tolerance=1e-4,
geometry_accuracy=1e-5,
boundary_layer_first_layer_thickness=1e-3,
),
volume_zones=[farfield],
),
models=[
Wall(entities=[surface_pos]), # OK
Wall(entities=[surface_neg]), # Error
Wall(entities=[surface_cross]), # OK (touches 0)
],
private_attribute_asset_cache=asset_cache,
)

_, errors, _ = validate_model(
params_as_dict=params.model_dump(mode="json"),
validated_by=ValidationCalledBy.LOCAL,
root_item_type="SurfaceMesh",
validation_level="All",
)

assert len(errors) == 1
assert "Boundary `neg_surf` will likely be deleted" in errors[0]["msg"]

# Test half_body_negative_y -> keeps negative, deletes positive
farfield_neg = UserDefinedFarfield(domain_type="half_body_negative_y")

with SI_unit_system:
params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
planar_face_tolerance=1e-4,
geometry_accuracy=1e-5,
boundary_layer_first_layer_thickness=1e-3,
),
volume_zones=[farfield_neg],
),
models=[
Wall(entities=[surface_pos]), # Error
Wall(entities=[surface_neg]), # OK
Wall(entities=[surface_cross]), # OK
],
private_attribute_asset_cache=asset_cache,
)

_, errors, _ = validate_model(
params_as_dict=params.model_dump(mode="json"),
validated_by=ValidationCalledBy.LOCAL,
root_item_type="SurfaceMesh",
validation_level="All",
)

assert len(errors) == 1
assert "Boundary `pos_surf` will likely be deleted" in errors[0]["msg"]