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
19 changes: 7 additions & 12 deletions flow360/component/simulation/meshing_param/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
UniformRefinement,
UserDefinedFarfield,
WindTunnelFarfield,
_FarfieldBase,
_FarfieldAllowingEnclosedEntities,
)
from flow360.component.simulation.primitives import (
AxisymmetricBody,
Expand Down Expand Up @@ -121,9 +121,11 @@ def _collect_rotation_entity_names(zones, param_info, zone_types):
def _validate_farfield_enclosed_entities(
zones, rotation_entity_names, has_custom_volumes, param_info
):
"""Validate farfield enclosed_entities: require CustomVolumes and rotation-volume association."""
"""Validate farfield enclosed_entities: require CustomVolumes and rotation-volume association.
Only applies to farfield types that support enclosed_entities (Automated, WindTunnel).
"""
for zone in zones:
if not isinstance(zone, _FarfieldBase):
if not isinstance(zone, _FarfieldAllowingEnclosedEntities):
continue

if zone.enclosed_entities is None:
Expand Down Expand Up @@ -269,10 +271,8 @@ def _check_volume_zones_has_farfield(cls, v):
)
for volume_zone in v
)
if total_farfield == 0 and not _collect_all_custom_volumes(v):
raise ValueError(
"A farfield zone or `CustomVolume` entities are required in `volume_zones`."
)
if total_farfield == 0:
raise ValueError("Farfield zone is required in `volume_zones`.")

if total_farfield > 1:
raise ValueError("Only one farfield zone is allowed in `volume_zones`.")
Expand Down Expand Up @@ -507,18 +507,13 @@ def _warn_multi_zone_remove_hidden_geometry(self) -> Self:
def farfield_method(self):
"""Returns the farfield method used."""
if self.volume_zones:
has_custom_zones = False
for zone in self.volume_zones: # pylint: disable=not-an-iterable
if isinstance(zone, AutomatedFarfield):
return zone.method
if isinstance(zone, WindTunnelFarfield):
return "wind-tunnel"
if isinstance(zone, UserDefinedFarfield):
return "user-defined"
if isinstance(zone, CustomZones):
has_custom_zones = True
if has_custom_zones: # CV + no FF => implicit UD
return "user-defined"
return None


Expand Down
115 changes: 60 additions & 55 deletions flow360/component/simulation/meshing_param/volume_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,59 +563,6 @@ class _FarfieldBase(Flow360BaseModel):
)
)

enclosed_entities: Optional[
EntityList[Surface, Cylinder, AxisymmetricBody, Sphere, CustomVolume]
] = pd.Field(
None,
description="""
The surfaces/surface groups that are the interior boundaries of the `farfield` zone when defining custom volumes.
- Only allowed when using one or more `CustomZone`(s) to define volume zone(s) in meshing parameters
- Cylinder, AxisymmetricBody, Sphere entities must be associated with `RotationVolume`(s)
""",
)

@contextual_model_validator(mode="after")
def _validate_enclosed_entities_no_intersection(self, param_info: ParamsValidationInfo):
"""Check that no CustomVolume's bounding_entities overlap with sibling entities."""
if self.enclosed_entities is None:
return self
expanded = param_info.expand_entity_list(self.enclosed_entities)

custom_volumes_in_list = [e for e in expanded if isinstance(e, CustomVolume)]
if not custom_volumes_in_list:
return self

non_cv_names = {e.name for e in expanded if not isinstance(e, CustomVolume)}

for cv in custom_volumes_in_list:
cv_child_names = {e.name for e in param_info.expand_entity_list(cv.bounding_entities)}
overlap = cv_child_names & non_cv_names
if overlap:
raise ValueError(
f"`CustomVolume` `{cv.name}` shares bounding entities "
f"({sorted(overlap)}) with sibling `CustomVolume`. "
f"A `CustomVolume`'s bounding entities must be disjoint from its siblings."
)

return self

@contextual_field_validator("enclosed_entities", mode="after")
@classmethod
def _validate_enclosed_entities_beta_mesher_only(cls, value, param_info: ParamsValidationInfo):
"""Ensure enclosed_entities is only used with the beta mesher."""
if value is None:
return value
if param_info.is_beta_mesher:
return value

raise ValueError("`enclosed_entities` is only supported with the beta mesher.")

@contextual_field_validator("enclosed_entities", mode="after")
@classmethod
def _validate_enclosed_entity_existence(cls, value, param_info: ParamsValidationInfo):
"""Ensure all boundaries will be present after mesher."""
return validate_entity_list_surface_existence(value, param_info)

@contextual_field_validator("domain_type", mode="after")
@classmethod
def _validate_only_in_beta_mesher(cls, value, param_info: ParamsValidationInfo):
Expand Down Expand Up @@ -679,7 +626,65 @@ def _validate_domain_type_bbox(cls, value):
raise ValueError(message)


class AutomatedFarfield(_FarfieldBase):
class _FarfieldAllowingEnclosedEntities(_FarfieldBase):
"""Intermediate class for farfield types that support enclosed_entities (Automated, WindTunnel)."""

enclosed_entities: Optional[
EntityList[Surface, Cylinder, AxisymmetricBody, Sphere, CustomVolume]
] = pd.Field(
None,
description="""
The surfaces/surface groups that are the interior boundaries of the `farfield` zone when defining custom volumes.
- Only allowed when using one or more `CustomZone`(s) to define volume zone(s) in meshing parameters
- Cylinder, AxisymmetricBody, Sphere entities must be associated with `RotationVolume`(s)
""",
)

@contextual_field_validator("enclosed_entities", mode="after")
@classmethod
def _validate_enclosed_entities_no_intersection(cls, value, param_info: ParamsValidationInfo):
"""Check that no CustomVolume's bounding_entities overlap with sibling entities."""
if value is None:
return value
expanded = param_info.expand_entity_list(value)

custom_volumes_in_list = [e for e in expanded if isinstance(e, CustomVolume)]
if not custom_volumes_in_list:
return value

non_cv_names = {e.name for e in expanded if not isinstance(e, CustomVolume)}

for cv in custom_volumes_in_list:
cv_child_names = {e.name for e in param_info.expand_entity_list(cv.bounding_entities)}
overlap = cv_child_names & non_cv_names
if overlap:
raise ValueError(
f"`CustomVolume` `{cv.name}` shares bounding entities "
f"({sorted(overlap)}) with sibling `CustomVolume`. "
f"A `CustomVolume`'s bounding entities must be disjoint from its siblings."
)

return value

@contextual_field_validator("enclosed_entities", mode="after")
@classmethod
def _validate_enclosed_entities_beta_mesher_only(cls, value, param_info: ParamsValidationInfo):
"""Ensure enclosed_entities is only used with the beta mesher."""
if value is None:
return value
if param_info.is_beta_mesher:
return value

raise ValueError("`enclosed_entities` is only supported with the beta mesher.")

@contextual_field_validator("enclosed_entities", mode="after")
@classmethod
def _validate_enclosed_entity_existence(cls, value, param_info: ParamsValidationInfo):
"""Ensure all boundaries will be present after mesher."""
return validate_entity_list_surface_existence(value, param_info)


class AutomatedFarfield(_FarfieldAllowingEnclosedEntities):
"""
Settings for automatic farfield volume zone generation.

Expand Down Expand Up @@ -898,7 +903,7 @@ def _validate_wheel_belt_ranges(self):


# pylint: disable=no-member
class WindTunnelFarfield(_FarfieldBase):
class WindTunnelFarfield(_FarfieldAllowingEnclosedEntities):
"""
Settings for analytic wind tunnel farfield generation.
The user only needs to provide tunnel dimensions and floor type and dimensions, rather than a geometry.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
UniformRefinement,
UserDefinedFarfield,
WindTunnelFarfield,
_FarfieldBase,
_FarfieldAllowingEnclosedEntities,
)
from flow360.component.simulation.primitives import (
AxisymmetricBody,
Expand Down Expand Up @@ -262,13 +262,16 @@ def rotation_volume_entity_injector(


def _build_farfield_zone(volume_zones: list):
"""Build the farfield zone dict from enclosed_entities on any farfield type.
"""Build the farfield zone dict from enclosed_entities on any farfield type supporting it.

CustomVolume entities are unwrapped into their constituent bounding_entities,
each translated via _translate_enclosed_entity_name. Final patches are deduplicated.
"""
for zone in volume_zones:
if isinstance(zone, _FarfieldBase) and zone.enclosed_entities is not None:
if (
isinstance(zone, _FarfieldAllowingEnclosedEntities)
and zone.enclosed_entities is not None
):
patch_names: set[str] = set()
for entity in zone.enclosed_entities.stored_entities:
if isinstance(entity, CustomVolume):
Expand Down
17 changes: 11 additions & 6 deletions flow360/component/simulation/validation/validation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class ParamsValidationInfo: # pylint:disable=too-few-public-methods,too-many-in
@classmethod
def _get_farfield_method_(cls, param_as_dict: dict):
meshing = param_as_dict.get("meshing")
modular = False
if meshing is None:
# No meshing info.
return None
Expand All @@ -164,19 +165,24 @@ def _get_farfield_method_(cls, param_as_dict: dict):
volume_zones = meshing.get("volume_zones")
else:
volume_zones = meshing.get("zones")
modular = True
if volume_zones:
has_custom_zones = False
for zone in volume_zones:
if zone["type"] == "AutomatedFarfield":
return zone["method"]
if zone["type"] == "UserDefinedFarfield":
return "user-defined"
if zone["type"] == "WindTunnelFarfield":
return "wind-tunnel"
if zone["type"] in ("CustomZones", "SeedpointVolume"):
has_custom_zones = True
if has_custom_zones: # CV + no FF => implicit UD
return "user-defined"
if (
zone["type"]
in [
"CustomZones",
"SeedpointVolume",
]
and modular
):
return "user-defined"

return None

Expand Down Expand Up @@ -444,7 +450,6 @@ def _get_farfield_enclosed_entities(self, param_as_dict: dict) -> dict[str, str]
for zone in volume_zones:
if zone.get("type") not in (
"AutomatedFarfield",
"UserDefinedFarfield",
"WindTunnelFarfield",
):
continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,12 @@ def _collect_asset_boundary_entities(params, param_info: ParamsValidationInfo) -
if param_info.will_generate_forced_symmetry_plane():
asset_boundary_entities += [item for item in ghost_entities if item.name == "symmetric"]
# pylint: disable=protected-access
wind_tunnel = next(
z for z in params.meshing.volume_zones if isinstance(z, WindTunnelFarfield)
)
asset_boundary_entities += WindTunnelFarfield._get_valid_ghost_surfaces(
params.meshing.volume_zones[0].floor_type.type_name,
params.meshing.volume_zones[0].domain_type,
wind_tunnel.floor_type.type_name,
wind_tunnel.domain_type,
)

return asset_boundary_entities, has_missing_private_attributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2507,6 +2507,7 @@ def test_multi_zone_remove_hidden_geometry_warning():
remove_hidden_geometry=True,
),
volume_zones=[
UserDefinedFarfield(),
CustomZones(
name="custom_zones",
entities=[
Expand Down Expand Up @@ -2551,9 +2552,7 @@ def test_multi_zone_remove_hidden_geometry_warning():
remove_hidden_geometry=True,
),
volume_zones=[
UserDefinedFarfield(
enclosed_entities=[Surface(name="face1"), Surface(name="face2")]
),
UserDefinedFarfield(),
CustomZones(
name="custom_zones",
entities=[
Expand Down Expand Up @@ -2590,9 +2589,7 @@ def test_multi_zone_remove_hidden_geometry_warning():
remove_hidden_geometry=True,
),
volume_zones=[
UserDefinedFarfield(
enclosed_entities=[Surface(name="face1"), Surface(name="face2")]
),
UserDefinedFarfield(),
CustomZones(
name="custom_zones",
entities=[
Expand Down Expand Up @@ -2637,6 +2634,7 @@ def test_multi_zone_remove_hidden_geometry_warning():
remove_hidden_geometry=True,
),
volume_zones=[
UserDefinedFarfield(),
CustomZones(
name="custom_zones",
entities=[
Expand Down
Loading
Loading