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
46 changes: 46 additions & 0 deletions flow360/component/simulation/meshing_param/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,52 @@ def _check_volume_zones_has_farfield(cls, v):

return v

@contextual_field_validator("volume_zones", mode="after")
@classmethod
def _check_automated_farfield_custom_volumes(cls, v, param_info):
if v is None:
return v

automated_farfield = next((zone for zone in v if isinstance(zone, AutomatedFarfield)), None)
Comment thread
benflexcompute marked this conversation as resolved.
if automated_farfield is not None:
custom_volumes = [
entity
for zone in v
if isinstance(zone, CustomZones)
for entity in param_info.expand_entity_list(zone.entities)
if isinstance(entity, CustomVolume)
]
if any(cv.name == "farfield" for cv in custom_volumes):
Comment thread
benflexcompute marked this conversation as resolved.
raise ValueError(
"CustomVolume name 'farfield' is reserved when using AutomatedFarfield. "
"The 'farfield' zone will be automatically generated using `AutomatedFarfield.enclosed_surfaces`. "
"Please choose a different name."
)
Comment thread
alexxu-flex marked this conversation as resolved.

enclosed_surfaces = (
param_info.expand_entity_list(automated_farfield.enclosed_surfaces)
if automated_farfield.enclosed_surfaces is not None
else []
)

if custom_volumes and not enclosed_surfaces:
raise ValueError(
"When using AutomatedFarfield with CustomVolumes, `enclosed_surfaces` must be "
"specified on the AutomatedFarfield to define the exterior farfield zone boundary."
)
if enclosed_surfaces and not custom_volumes:
raise ValueError(
"`enclosed_surfaces` on AutomatedFarfield is only allowed when CustomVolume entities are used. "
"Without custom volumes, the farfield zone will be automatically detected."
Comment thread
alexxu-flex marked this conversation as resolved.
)
if any(s.name == "farfield" for s in enclosed_surfaces):
raise ValueError(
"Surface name 'farfield' in `enclosed_surfaces` will conflict with the automatically "
"generated farfield boundary. Please choose a different surface."
)

return v

@contextual_field_validator("volume_zones", mode="after")
@classmethod
def _check_volume_zones_have_unique_names(cls, v):
Expand Down
7 changes: 7 additions & 0 deletions flow360/component/simulation/meshing_param/volume_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,13 @@ class AutomatedFarfield(_FarfieldBase):
description="Radius of the far-field (semi)sphere/cylinder relative to "
"the max dimension of the geometry bounding box.",
)
enclosed_surfaces: Optional[EntityList[Surface]] = pd.Field(
Comment thread
benflexcompute marked this conversation as resolved.
None,
description=(
"Geometry surfaces that, together with the farfield surface, form the boundary of the "
"exterior farfield zone. Required when using CustomVolumes alongside an AutomatedFarfield. "
),
)

@property
def farfield(self):
Expand Down
5 changes: 3 additions & 2 deletions flow360/component/simulation/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,15 +1084,16 @@ def ensure_unique_boundary_names(cls, v, param_info: ParamsValidationInfo):

@contextual_model_validator(mode="after")
def ensure_beta_mesher_and_compatible_farfield(self, param_info: ParamsValidationInfo):
"""Check if the beta mesher is enabled and that the user is using user-defined or wind tunnel farfield."""
"""Check if the beta mesher is enabled and that the user is using a compatible farfield."""
if param_info.is_beta_mesher and param_info.farfield_method in (
"user-defined",
"wind-tunnel",
"auto",
Comment thread
alexxu-flex marked this conversation as resolved.
):
return self
raise ValueError(
"CustomVolume is supported only when the beta mesher is enabled "
"and either a user-defined farfield or a wind tunnel farfield is enabled."
"and an automated, user-defined, or wind tunnel farfield is enabled."
)

def _apply_transformation(self, matrix: np.ndarray) -> "CustomVolume":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,18 @@ def _get_custom_volumes(volume_zones: list):
}
)

# Create "farfield" zone from enclosed_surfaces on AutomatedFarfield
for zone in volume_zones:
if isinstance(zone, AutomatedFarfield) and zone.enclosed_surfaces is not None:
Comment thread
benflexcompute marked this conversation as resolved.
patch_names = [surface.name for surface in zone.enclosed_surfaces.stored_entities]
Comment thread
alexxu-flex marked this conversation as resolved.
Comment thread
alexxu-flex marked this conversation as resolved.
custom_volumes.append(
{
"name": "farfield",
"patches": sorted(patch_names),
}
Comment thread
cursor[bot] marked this conversation as resolved.
)
break

if custom_volumes:
# Sort custom volumes by name
custom_volumes.sort(key=lambda x: x["name"])
Expand Down Expand Up @@ -418,6 +430,7 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units):

##:: Step 2: Get farfield
for zone in volume_zones:
# CustomZone implies user-defined farfield if no farfield is specified anywhere
if isinstance(zone, (UserDefinedFarfield, CustomZones)):
translated["farfield"] = {"type": "user-defined"}
if hasattr(zone, "domain_type") and zone.domain_type is not None:
Expand Down
164 changes: 153 additions & 11 deletions tests/simulation/params/test_validators_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2210,7 +2210,7 @@ def test_beta_mesher_only_features(mock_validation_context):
)
assert errors is None

# Using CustomZones with all three farfield types
# Using CustomZones with WindTunnelFarfield
with SI_unit_system:
params = SimulationParams(
meshing=MeshingParams(
Expand Down Expand Up @@ -2279,6 +2279,8 @@ def test_beta_mesher_only_features(mock_validation_context):
)

with SI_unit_system:
surface1 = Surface(name="face1")
surface2 = Surface(name="face2")
params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
Expand All @@ -2291,11 +2293,14 @@ def test_beta_mesher_only_features(mock_validation_context):
entities=[
CustomVolume(
name="zone1",
boundaries=[Surface(name="face1"), Surface(name="face2")],
)
boundaries=[surface1, surface2],
),
],
),
AutomatedFarfield(),
AutomatedFarfield(
method="auto",
enclosed_surfaces=[surface1, surface2],
),
],
),
private_attribute_asset_cache=AssetCache(use_inhouse_mesher=True),
Expand All @@ -2306,12 +2311,7 @@ def test_beta_mesher_only_features(mock_validation_context):
root_item_type="SurfaceMesh",
validation_level="VolumeMesh",
)
assert len(errors) == 1
assert (
errors[0]["msg"]
== "Value error, CustomVolume is supported only when the beta mesher is enabled "
+ "and either a user-defined farfield or a wind tunnel farfield is enabled."
)
assert errors is None

with SI_unit_system:
params = SimulationParams(
Expand Down Expand Up @@ -2345,7 +2345,7 @@ def test_beta_mesher_only_features(mock_validation_context):
assert (
errors[0]["msg"]
== "Value error, CustomVolume is supported only when the beta mesher is enabled "
+ "and either a user-defined farfield or a wind tunnel farfield is enabled."
+ "and an automated, user-defined, or wind tunnel farfield is enabled."
)

# Unique volume zone names
Expand Down Expand Up @@ -3478,3 +3478,145 @@ def test_incomplete_BC_without_geometry_AI():
"Value error, The following boundaries do not have a boundary condition: no_bc. "
"Please add them to a boundary condition model in the `models` section."
)


def test_automated_farfield_with_custom_zones():
"""AutomatedFarfield + CustomZones: enclosed_surfaces is required when CustomVolumes exist."""

# Positive: enclosed_surfaces provided with CustomVolumes -> should pass
with SI_unit_system:
params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
boundary_layer_first_layer_thickness=1e-4,
),
volume_zones=[
CustomZones(
name="interior",
entities=[
CustomVolume(
name="zone1",
boundaries=[
Surface(name="face1"),
Surface(name="face2"),
],
)
],
),
AutomatedFarfield(
enclosed_surfaces=[
Surface(name="face1"),
Surface(name="face2"),
],
),
],
),
private_attribute_asset_cache=AssetCache(use_inhouse_mesher=True),
)
params, errors, _ = validate_model(
params_as_dict=params.model_dump(mode="json"),
validated_by=ValidationCalledBy.LOCAL,
root_item_type="SurfaceMesh",
validation_level="VolumeMesh",
)
assert errors is None

# Negative: CustomVolumes exist but enclosed_surfaces not provided -> should fail
with SI_unit_system:
params_no_enclosed = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
boundary_layer_first_layer_thickness=1e-4,
),
volume_zones=[
CustomZones(
name="inner",
entities=[
CustomVolume(
name="zone1",
boundaries=[Surface(name="face1"), Surface(name="face2")],
)
],
),
AutomatedFarfield(),
],
),
private_attribute_asset_cache=AssetCache(use_inhouse_mesher=True),
)
_, errors, _ = validate_model(
params_as_dict=params_no_enclosed.model_dump(mode="json"),
validated_by=ValidationCalledBy.LOCAL,
root_item_type="SurfaceMesh",
validation_level="VolumeMesh",
)
assert (
errors[0]["msg"]
== "Value error, When using AutomatedFarfield with CustomVolumes, `enclosed_surfaces` must be "
"specified on the AutomatedFarfield to define the exterior farfield zone boundary."
)

# Negative: enclosed_surfaces provided but no CustomVolumes -> should fail
with SI_unit_system:
params_no_cv = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
boundary_layer_first_layer_thickness=1e-4,
),
volume_zones=[
AutomatedFarfield(
enclosed_surfaces=[Surface(name="face1")],
),
],
),
private_attribute_asset_cache=AssetCache(use_inhouse_mesher=True),
)
_, errors, _ = validate_model(
params_as_dict=params_no_cv.model_dump(mode="json"),
validated_by=ValidationCalledBy.LOCAL,
root_item_type="SurfaceMesh",
validation_level="VolumeMesh",
)
assert (
errors[0]["msg"]
== "Value error, `enclosed_surfaces` on AutomatedFarfield is only allowed when CustomVolume "
"entities are used. Without custom volumes, the farfield zone will be automatically detected."
)


def test_custom_volume_named_farfield_with_automated_farfield():
"""CustomVolume named 'farfield' is reserved when using AutomatedFarfield."""
with SI_unit_system:
params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
boundary_layer_first_layer_thickness=1e-4,
),
volume_zones=[
CustomZones(
name="zones",
entities=[
CustomVolume(
name="farfield",
boundaries=[Surface(name="face1"), Surface(name="face2")],
)
],
),
AutomatedFarfield(
enclosed_surfaces=[Surface(name="face1"), Surface(name="face2")],
),
],
),
private_attribute_asset_cache=AssetCache(use_inhouse_mesher=True),
)
_, errors, _ = validate_model(
params_as_dict=params.model_dump(mode="json"),
validated_by=ValidationCalledBy.LOCAL,
root_item_type="SurfaceMesh",
validation_level="VolumeMesh",
)
assert (
errors[0]["msg"]
== "Value error, CustomVolume name 'farfield' is reserved when using AutomatedFarfield. "
"The 'farfield' zone will be automatically generated using `AutomatedFarfield.enclosed_surfaces`. "
"Please choose a different name."
)
37 changes: 36 additions & 1 deletion tests/simulation/translator/test_volume_meshing_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
AxisymmetricRefinement,
CustomZones,
MeshSliceOutput,
RotationCylinder,
RotationVolume,
StructuredBoxRefinement,
UniformRefinement,
Expand Down Expand Up @@ -1315,3 +1314,39 @@ def test_sphere_rotation_volume_translator(get_surface_mesh):
assert outer_interface["maxEdgeLength"] == 0.5
assert "slidingInterface-sphereInterface" in outer_interface["enclosedObjects"]
assert "otherBody" in outer_interface["enclosedObjects"]


def test_automated_farfield_enclosed_surfaces(get_surface_mesh):
"""AutomatedFarfield.enclosed_surfaces should create a 'farfield' zone in translated output."""
left1 = Surface(name="left1")
right1 = Surface(name="right1")
with SI_unit_system:
params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
boundary_layer_first_layer_thickness=1e-4,
),
volume_zones=[
CustomZones(
name="interior_zone",
entities=[
CustomVolume(
name="inner",
boundaries=[left1, right1],
),
],
),
AutomatedFarfield(
enclosed_surfaces=[left1, right1],
),
],
),
private_attribute_asset_cache=AssetCache(use_inhouse_mesher=True),
)

translated = get_volume_meshing_json(params, get_surface_mesh.mesh_unit)
assert "zones" in translated
zones_by_name = {z["name"]: z for z in translated["zones"]}
# enclosed_surfaces should produce a "farfield" zone
assert "farfield" in zones_by_name
assert sorted(zones_by_name["farfield"]["patches"]) == ["left1", "right1"]
Loading