diff --git a/flow360/__init__.py b/flow360/__init__.py index 18a7b1207..4e1d0831e 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -38,13 +38,18 @@ from flow360.component.simulation.meshing_param.volume_params import ( AutomatedFarfield, AxisymmetricRefinement, + CentralBelt, CustomZones, + FullyMovingFloor, MeshSliceOutput, RotationCylinder, RotationVolume, + StaticFloor, StructuredBoxRefinement, UniformRefinement, UserDefinedFarfield, + WheelBelts, + WindTunnelFarfield, ) from flow360.component.simulation.models.material import ( Air, @@ -337,6 +342,11 @@ "ImportedSurface", "OctreeSpacing", "RunControl", + "WindTunnelFarfield", + "StaticFloor", + "FullyMovingFloor", + "CentralBelt", + "WheelBelts", ] _warn_prerelease() diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index 56579aae0..1a66a8b23 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -29,6 +29,7 @@ GhostSphere, SnappyBody, Surface, + WindTunnelGhostSurface, ) from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.utils import BoundingBoxType, model_attribute_unlock @@ -53,11 +54,11 @@ class EntityInfoModel(Flow360BaseModel, metaclass=ABCMeta): """Base model for asset entity info JSON""" - # entities that appear in simulation JSON but did not appear in EntityInfo) + # entities that appear in simulation JSON but did not appear in EntityInfo draft_entities: List[DraftEntityTypes] = pd.Field([]) ghost_entities: List[ Annotated[ - Union[GhostSphere, GhostCircularPlane], + Union[GhostSphere, GhostCircularPlane, WindTunnelGhostSurface], pd.Field(discriminator="private_attribute_entity_type_name"), ] ] = pd.Field([]) diff --git a/flow360/component/simulation/meshing_param/params.py b/flow360/component/simulation/meshing_param/params.py index daa909e84..c68288b1b 100644 --- a/flow360/component/simulation/meshing_param/params.py +++ b/flow360/component/simulation/meshing_param/params.py @@ -30,6 +30,7 @@ StructuredBoxRefinement, UniformRefinement, UserDefinedFarfield, + WindTunnelFarfield, ) from flow360.component.simulation.primitives import SeedpointVolume from flow360.component.simulation.validation.validation_context import ( @@ -60,6 +61,7 @@ AutomatedFarfield, UserDefinedFarfield, CustomZones, + WindTunnelFarfield, ], pd.Field(discriminator="type"), ] @@ -162,13 +164,14 @@ class MeshingParams(Flow360BaseModel): @pd.field_validator("volume_zones", mode="after") @classmethod - def _check_volume_zones_has_farfied(cls, v): + def _check_volume_zones_has_farfield(cls, v): if v is None: # User did not put anything in volume_zones so may not want to use volume meshing return v total_farfield = sum( - isinstance(volume_zone, (AutomatedFarfield, UserDefinedFarfield)) for volume_zone in v + isinstance(volume_zone, (AutomatedFarfield, WindTunnelFarfield, UserDefinedFarfield)) + for volume_zone in v ) if total_farfield == 0: raise ValueError("Farfield zone is required in `volume_zones`.") @@ -275,11 +278,13 @@ def _check_no_reused_volume_entities(self) -> Self: @property def farfield_method(self): - """Returns the farfield method used.""" + """Returns the farfield method used.""" if self.volume_zones: 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" return None diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index 411f7bb69..ed792a31f 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -3,11 +3,12 @@ """ from abc import ABCMeta -from typing import Literal, Optional +from typing import Literal, Optional, Union import pydantic as pd from typing_extensions import deprecated +import flow360.component.simulation.units as u from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.framework.entity_base import EntityList from flow360.component.simulation.outputs.output_entities import Slice @@ -20,6 +21,7 @@ GhostSurface, SeedpointVolume, Surface, + WindTunnelGhostSurface, ) from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.validation.validation_context import ( @@ -28,6 +30,7 @@ from flow360.component.simulation.validation.validation_utils import ( check_deleted_surface_in_entity_list, ) +from flow360.exceptions import Flow360ValueError class UniformRefinement(Flow360BaseModel): @@ -358,6 +361,9 @@ class RotationCylinder(RotationVolume): entities: EntityList[Cylinder] = pd.Field() +### BEGIN FARFIELDS ### + + class _FarfieldBase(Flow360BaseModel): """Base class for farfield parameters.""" @@ -495,7 +501,7 @@ def symmetry_plane(self) -> GhostSurface: """ if self.method == "auto": return GhostSurface(name="symmetric") - raise ValueError( + raise Flow360ValueError( "Unavailable for quasi-3d farfield methods. Please use `symmetry_planes` property instead." ) @@ -510,7 +516,7 @@ def symmetry_planes(self): GhostSurface(name="symmetric-1"), GhostSurface(name="symmetric-2"), ] - raise ValueError(f"Unsupported method: {self.method}") + raise Flow360ValueError(f"Unsupported method: {self.method}") @pd.field_validator("method", mode="after") @classmethod @@ -551,13 +557,366 @@ def symmetry_plane(self) -> GhostSurface: Warning: This should only be used when using GAI and beta mesher. """ if self.domain_type not in ("half_body_positive_y", "half_body_negative_y"): - raise ValueError( + raise Flow360ValueError( "Symmetry plane of user defined farfield is only supported when domain_type " "is `half_body_positive_y` or `half_body_negative_y`." ) return GhostSurface(name="symmetric") +# pylint: disable=no-member +class StaticFloor(Flow360BaseModel): + """Class for static wind tunnel floor with friction patch.""" + + type_name: Literal["StaticFloor"] = pd.Field( + "StaticFloor", description="Static floor with friction patch.", frozen=True + ) + friction_patch_x_range: LengthType.Range = pd.Field( + default=(-3, 6) * u.m, description="(Minimum, maximum) x of friction patch." + ) + friction_patch_width: LengthType.Positive = pd.Field( + default=2 * u.m, description="Width of friction patch." + ) + + +class FullyMovingFloor(Flow360BaseModel): + """Class for fully moving wind tunnel floor with friction patch.""" + + type_name: Literal["FullyMovingFloor"] = pd.Field( + "FullyMovingFloor", description="Fully moving floor.", frozen=True + ) + + +# pylint: disable=no-member +class CentralBelt(Flow360BaseModel): + """Class for wind tunnel floor with one central belt.""" + + type_name: Literal["CentralBelt"] = pd.Field( + "CentralBelt", description="Floor with central belt.", frozen=True + ) + central_belt_x_range: LengthType.Range = pd.Field( + default=(-2, 2) * u.m, description="(Minimum, maximum) x of central belt." + ) + central_belt_width: LengthType.Positive = pd.Field( + default=1.2 * u.m, description="Width of central belt." + ) + + +class WheelBelts(CentralBelt): + """Class for wind tunnel floor with one central belt and four wheel belts.""" + + type_name: Literal["WheelBelts"] = pd.Field( + "WheelBelts", description="Floor with central belt and four wheel belts.", frozen=True + ) + # No defaults for the below; user must specify + front_wheel_belt_x_range: LengthType.Range = pd.Field( + description="(Minimum, maximum) x of front wheel belt." + ) + front_wheel_belt_y_range: LengthType.PositiveRange = pd.Field( + description="(Inner, outer) y of front wheel belt." + ) + rear_wheel_belt_x_range: LengthType.Range = pd.Field( + description="(Minimum, maximum) x of rear wheel belt." + ) + rear_wheel_belt_y_range: LengthType.PositiveRange = pd.Field( + description="(Inner, outer) y of rear wheel belt." + ) + + @pd.model_validator(mode="after") + def _validate_wheel_belt_ranges(self): + if self.front_wheel_belt_x_range[1] >= self.rear_wheel_belt_x_range[0]: + raise ValueError( + f"Front wheel belt maximum x ({self.front_wheel_belt_x_range[1]}) " + f"must be less than rear wheel belt minimum x ({self.rear_wheel_belt_x_range[0]})." + ) + return self + + +# pylint: disable=no-member +class WindTunnelFarfield(_FarfieldBase): + """ + Settings for analytic wind tunnel farfield generation. + The user only needs to provide tunnel dimensions and floor type and dimensions, rather than a geometry. + + Example + ------- + >>> fl.WindTunnelFarfield( + width = 10 * fl.u.m, + height = 5 * fl.u.m, + inlet_x_position = -10 * fl.u.m, + outlet_x_position = 20 * fl.u.m, + floor_z_position = 0 * fl.u.m, + floor_type = fl.CentralBelt( + central_belt_x_range = (-1, 4) * fl.u.m, + central_belt_width = 1.2 * fl.u.m + ) + ) + """ + + type: Literal["WindTunnelFarfield"] = pd.Field("WindTunnelFarfield", frozen=True) + name: str = pd.Field("Wind Tunnel Farfield", description="Name of the wind tunnel farfield.") + + # Tunnel parameters + width: LengthType.Positive = pd.Field(default=10 * u.m, description="Width of the wind tunnel.") + height: LengthType.Positive = pd.Field( + default=6 * u.m, description="Height of the wind tunnel." + ) + inlet_x_position: LengthType = pd.Field( + default=-20 * u.m, description="X-position of the inlet." + ) + outlet_x_position: LengthType = pd.Field( + default=40 * u.m, description="X-position of the outlet." + ) + floor_z_position: LengthType = pd.Field(default=0 * u.m, description="Z-position of the floor.") + + floor_type: Union[ + StaticFloor, + FullyMovingFloor, + CentralBelt, + WheelBelts, + ] = pd.Field( + default_factory=StaticFloor, + description="Floor type of the wind tunnel.", + discriminator="type_name", + ) + + # up direction not yet supported; assume +Z + + @property + def symmetry_plane(self) -> GhostSurface: + """ + Returns the symmetry plane boundary surface for half body domains. + """ + if self.domain_type not in ("half_body_positive_y", "half_body_negative_y"): + raise Flow360ValueError( + "Symmetry plane for wind tunnel farfield is only supported when domain_type " + "is `half_body_positive_y` or `half_body_negative_y`." + ) + return GhostSurface(name="symmetric") + + @staticmethod + def _left(): + return WindTunnelGhostSurface(name="windTunnelLeft") + + @property + def left(self) -> WindTunnelGhostSurface: + """Returns the left boundary surface.""" + if self.domain_type == "half_body_positive_y": + raise Flow360ValueError( + "Left boundary for wind tunnel farfield is not applicable when domain_type " + "is `half_body_positive_y`." + ) + return WindTunnelFarfield._left() + + @staticmethod + def _right(): + return WindTunnelGhostSurface(name="windTunnelRight") + + @property + def right(self) -> WindTunnelGhostSurface: + """Returns the right boundary surface.""" + if self.domain_type == "half_body_negative_y": + raise Flow360ValueError( + "Right boundary for wind tunnel farfield is not applicable when domain_type " + "is `half_body_negative_y`." + ) + return WindTunnelFarfield._right() + + @staticmethod + def _inlet(): + return WindTunnelGhostSurface(name="windTunnelInlet") + + @property + def inlet(self) -> WindTunnelGhostSurface: + """Returns the inlet boundary surface.""" + return WindTunnelFarfield._inlet() + + @staticmethod + def _outlet(): + return WindTunnelGhostSurface(name="windTunnelOutlet") + + @property + def outlet(self) -> WindTunnelGhostSurface: + """Returns the outlet boundary surface.""" + return WindTunnelFarfield._outlet() + + @staticmethod + def _ceiling(): + return WindTunnelGhostSurface(name="windTunnelCeiling") + + @property + def ceiling(self) -> WindTunnelGhostSurface: + """Returns the ceiling boundary surface.""" + return WindTunnelFarfield._ceiling() + + @staticmethod + def _floor(): + return WindTunnelGhostSurface(name="windTunnelFloor") + + @property + def floor(self) -> WindTunnelGhostSurface: + """Returns the floor boundary surface, excluding friction, central, and wheel belts if applicable.""" + return WindTunnelFarfield._floor() + + @staticmethod + def _friction_patch(): + return WindTunnelGhostSurface(name="windTunnelFrictionPatch", used_by=["StaticFloor"]) + + @property + def friction_patch(self) -> WindTunnelGhostSurface: + """Returns the friction patch for StaticFloor floor type.""" + if not isinstance(self.floor_type, StaticFloor): + raise Flow360ValueError( + "Friction patch for wind tunnel farfield " + "is only supported if floor type is `StaticFloor`." + ) + return WindTunnelFarfield._friction_patch() + + @staticmethod + def _central_belt(): + return WindTunnelGhostSurface( + name="windTunnelCentralBelt", used_by=["CentralBelt", "WheelBelts"] + ) + + @property + def central_belt(self) -> WindTunnelGhostSurface: + """Returns the central belt for CentralBelt or WheelBelts floor types.""" + if not isinstance(self.floor_type, CentralBelt): + raise Flow360ValueError( + "Central belt for wind tunnel farfield " + "is only supported if floor type is `CentralBelt` or `WheelBelts`." + ) + return WindTunnelFarfield._central_belt() + + @staticmethod + def _front_wheel_belts(): + return WindTunnelGhostSurface(name="windTunnelFrontWheelBelt", used_by=["WheelBelts"]) + + @property + def front_wheel_belts(self) -> WindTunnelGhostSurface: + """Returns the front wheel belts for WheelBelts floor type.""" + if not isinstance(self.floor_type, WheelBelts): + raise Flow360ValueError( + "Front wheel belts for wind tunnel farfield " + "is only supported if floor type is `WheelBelts`." + ) + return WindTunnelFarfield._front_wheel_belts() + + @staticmethod + def _rear_wheel_belts(): + return WindTunnelGhostSurface(name="windTunnelRearWheelBelt", used_by=["WheelBelts"]) + + @property + def rear_wheel_belts(self) -> WindTunnelGhostSurface: + """Returns the rear wheel belts for WheelBelts floor type.""" + if not isinstance(self.floor_type, WheelBelts): + raise Flow360ValueError( + "Rear wheel belts for wind tunnel farfield " + "is only supported if floor type is `WheelBelts`." + ) + return WindTunnelFarfield._rear_wheel_belts() + + @staticmethod + def _get_valid_ghost_surfaces( + floor_string: Optional[str] = "all", domain_string: Optional[str] = None + ) -> list[WindTunnelGhostSurface]: + """ + Returns a list of valid ghost surfaces given a floor type as a string + or ``all``, and the domain type as a string. + """ + common_ghost_surfaces = [ + WindTunnelFarfield._inlet(), + WindTunnelFarfield._outlet(), + WindTunnelFarfield._ceiling(), + WindTunnelFarfield._floor(), + ] + if domain_string != "half_body_negative_y": + common_ghost_surfaces += [WindTunnelFarfield._right()] + if domain_string != "half_body_positive_y": + common_ghost_surfaces += [WindTunnelFarfield._left()] + for ghost_surface_type in [ + WindTunnelFarfield._friction_patch(), + WindTunnelFarfield._central_belt(), + WindTunnelFarfield._front_wheel_belts(), + WindTunnelFarfield._rear_wheel_belts(), + ]: + if floor_string == "all" or floor_string in ghost_surface_type.used_by: + common_ghost_surfaces += [ghost_surface_type] + return common_ghost_surfaces + + @pd.model_validator(mode="after") + def _validate_inlet_is_less_than_outlet(self): + if self.inlet_x_position >= self.outlet_x_position: + raise ValueError( + f"Inlet x position ({self.inlet_x_position}) " + f"must be less than outlet x position ({self.outlet_x_position})." + ) + return self + + @pd.model_validator(mode="after") + def _validate_central_belt_ranges(self): + # friction patch + if isinstance(self.floor_type, StaticFloor): + if self.floor_type.friction_patch_width >= self.width: + raise ValueError( + f"Friction patch width ({self.floor_type.friction_patch_width}) " + f"must be less than wind tunnel width ({self.width})" + ) + if self.floor_type.friction_patch_x_range[0] <= self.inlet_x_position: + raise ValueError( + f"Friction patch minimum x ({self.floor_type.friction_patch_x_range[0]}) " + f"must be greater than inlet x ({self.inlet_x_position})" + ) + if self.floor_type.friction_patch_x_range[1] >= self.outlet_x_position: + raise ValueError( + f"Friction patch maximum x ({self.floor_type.friction_patch_x_range[1]}) " + f"must be less than outlet x ({self.outlet_x_position})" + ) + # central belt + elif isinstance(self.floor_type, CentralBelt): + if self.floor_type.central_belt_width >= self.width: + raise ValueError( + f"Central belt width ({self.floor_type.central_belt_width}) " + f"must be less than wind tunnel width ({self.width})" + ) + if self.floor_type.central_belt_x_range[0] <= self.inlet_x_position: + raise ValueError( + f"Central belt minimum x ({self.floor_type.central_belt_x_range[0]}) " + f"must be greater than inlet x ({self.inlet_x_position})" + ) + if self.floor_type.central_belt_x_range[1] >= self.outlet_x_position: + raise ValueError( + f"Central belt maximum x ({self.floor_type.central_belt_x_range[1]}) " + f"must be less than outlet x ({self.outlet_x_position})" + ) + return self + + @pd.model_validator(mode="after") + def _validate_wheel_belts_ranges(self): + if isinstance(self.floor_type, WheelBelts): + if self.floor_type.front_wheel_belt_y_range[1] >= self.width * 0.5: + raise ValueError( + f"Front wheel outer y ({self.floor_type.front_wheel_belt_y_range[1]}) " + f"must be less than half of wind tunnel width ({self.width * 0.5})" + ) + if self.floor_type.rear_wheel_belt_y_range[1] >= self.width * 0.5: + raise ValueError( + f"Rear wheel outer y ({self.floor_type.rear_wheel_belt_y_range[1]}) " + f"must be less than half of wind tunnel width ({self.width * 0.5})" + ) + if self.floor_type.front_wheel_belt_x_range[0] <= self.inlet_x_position: + raise ValueError( + f"Front wheel minimum x ({self.floor_type.front_wheel_belt_x_range[0]}) " + f"must be greater than inlet x ({self.inlet_x_position})" + ) + if self.floor_type.rear_wheel_belt_x_range[1] >= self.outlet_x_position: + raise ValueError( + f"Rear wheel maximum x ({self.floor_type.rear_wheel_belt_x_range[1]}) " + f"must be less than outlet x ({self.outlet_x_position})" + ) + return self + + class MeshSliceOutput(Flow360BaseModel): """ :class:`MeshSliceOutput` class for mesh slice output settings. diff --git a/flow360/component/simulation/models/surface_models.py b/flow360/component/simulation/models/surface_models.py index c50a36f2c..11623921a 100644 --- a/flow360/component/simulation/models/surface_models.py +++ b/flow360/component/simulation/models/surface_models.py @@ -28,6 +28,7 @@ GhostSurfacePair, Surface, SurfacePair, + WindTunnelGhostSurface, ) from flow360.component.simulation.unit_system import ( AbsoluteTemperatureType, @@ -401,6 +402,11 @@ class Wall(BoundaryBase): ) private_attribute_dict: Optional[Dict] = pd.Field(None) + entities: EntityList[Surface, WindTunnelGhostSurface] = pd.Field( + alias="surfaces", + description="List of boundaries with the `Wall` boundary condition imposed.", + ) + @pd.model_validator(mode="after") def check_wall_function_conflict(self): """Check no setting is conflicting with the usage of wall function""" @@ -480,11 +486,11 @@ class Freestream(BoundaryBaseWithTurbulenceQuantities): + ":py:attr:`AerospaceCondition.alpha` and :py:attr:`AerospaceCondition.beta` angles. " + "Optionally, an expression for each of the velocity components can be specified.", ) - entities: EntityListAllowingGhost[Surface, GhostSurface, GhostSphere, GhostCircularPlane] = ( - pd.Field( - alias="surfaces", - description="List of boundaries with the `Freestream` boundary condition imposed.", - ) + entities: EntityListAllowingGhost[ + Surface, GhostSurface, WindTunnelGhostSurface, GhostSphere, GhostCircularPlane + ] = pd.Field( + alias="surfaces", + description="List of boundaries with the `Freestream` boundary condition imposed.", ) @pd.field_validator("velocity", mode="after") @@ -637,7 +643,9 @@ class SlipWall(BoundaryBase): "Slip wall", description="Name of the `SlipWall` boundary condition." ) type: Literal["SlipWall"] = pd.Field("SlipWall", frozen=True) - entities: EntityListAllowingGhost[Surface, GhostSurface, GhostCircularPlane] = pd.Field( + entities: EntityListAllowingGhost[ + Surface, GhostSurface, WindTunnelGhostSurface, GhostCircularPlane + ] = pd.Field( alias="surfaces", description="List of boundaries with the :code:`SlipWall` boundary condition imposed.", ) diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index fb15438c4..06d5724e7 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -38,6 +38,7 @@ GhostSurface, ImportedSurface, Surface, + WindTunnelGhostSurface, ) from flow360.component.simulation.unit_system import LengthType, TimeType from flow360.component.simulation.user_code.core.types import ( @@ -305,7 +306,12 @@ class SurfaceOutput(_AnimationAndFileFormatSettings): name: Optional[str] = pd.Field("Surface output", description="Name of the `SurfaceOutput`.") entities: EntityListAllowingGhost[ - Surface, GhostSurface, GhostCircularPlane, GhostSphere, ImportedSurface + Surface, + GhostSurface, + WindTunnelGhostSurface, + GhostCircularPlane, + GhostSphere, + ImportedSurface, ] = pd.Field( alias="surfaces", description="List of boundaries where output is generated.", @@ -633,7 +639,12 @@ class SurfaceIntegralOutput(_OutputBase): name: str = pd.Field("Surface integral output", description="Name of integral.") entities: EntityListAllowingGhost[ - Surface, GhostSurface, GhostCircularPlane, GhostSphere, ImportedSurface + Surface, + GhostSurface, + WindTunnelGhostSurface, + GhostCircularPlane, + GhostSphere, + ImportedSurface, ] = pd.Field( alias="surfaces", description="List of boundaries where the surface integral will be calculated.", diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index 91e99ec61..d023f4414 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -581,7 +581,9 @@ def _will_be_deleted_by_mesher( # 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"]], + farfield_method: Optional[ + Literal["auto", "quasi-3d", "quasi-3d-periodic", "user-defined", "wind-tunnel"] + ], global_bounding_box: Optional[BoundingBoxType], planar_face_tolerance: Optional[float], half_model_symmetry_plane_center_y: Optional[float], @@ -615,8 +617,8 @@ def _will_be_deleted_by_mesher( 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 + if farfield_method in ("user-defined", "wind-tunnel"): + # Not applicable to user defined or wind tunnel farfield return False if farfield_method == "auto": @@ -662,6 +664,18 @@ class GhostSurface(_SurfaceEntityBase): ) +class WindTunnelGhostSurface(GhostSurface): + """Wind tunnel boundary patches.""" + + private_attribute_entity_type_name: Literal["WindTunnelGhostSurface"] = pd.Field( + "WindTunnelGhostSurface", frozen=True + ) + # For frontend: list of floor types that use this boundary patch, or ["all"] + used_by: List[ + Literal["StaticFloor", "FullyMovingFloor", "CentralBelt", "WheelBelts", "all"] + ] = pd.Field(default_factory=lambda: ["all"], frozen=True) + + # pylint: disable=missing-class-docstring @final class GhostSphere(_SurfaceEntityBase): diff --git a/flow360/component/simulation/translator/surface_meshing_translator.py b/flow360/component/simulation/translator/surface_meshing_translator.py index 3ab3be545..82383b5e8 100644 --- a/flow360/component/simulation/translator/surface_meshing_translator.py +++ b/flow360/component/simulation/translator/surface_meshing_translator.py @@ -578,7 +578,7 @@ def _get_volume_zones(volume_zones_list: list[dict]): return [ item for item in volume_zones_list - if item["type"] in ("AutomatedFarfield", "UserDefinedFarfield") + if item["type"] in ("AutomatedFarfield", "UserDefinedFarfield", "WindTunnelFarfield") ] diff --git a/flow360/component/simulation/translator/volume_meshing_translator.py b/flow360/component/simulation/translator/volume_meshing_translator.py index 137de54c9..0c1cc37d5 100644 --- a/flow360/component/simulation/translator/volume_meshing_translator.py +++ b/flow360/component/simulation/translator/volume_meshing_translator.py @@ -22,6 +22,7 @@ StructuredBoxRefinement, UniformRefinement, UserDefinedFarfield, + WindTunnelFarfield, ) from flow360.component.simulation.primitives import ( AxisymmetricBody, @@ -367,6 +368,12 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units): if hasattr(zone, "domain_type") and zone.domain_type is not None: translated["farfield"]["domainType"] = zone.domain_type + if isinstance(zone, WindTunnelFarfield): + translated["farfield"] = {"type": "wind-tunnel"} + if zone.domain_type is not None: + translated["farfield"]["domainType"] = zone.domain_type + break + if isinstance(zone, AutomatedFarfield): translated["farfield"] = { "planarFaceTolerance": planar_tolerance, diff --git a/flow360/component/simulation/unit_system.py b/flow360/component/simulation/unit_system.py index ec5fcc95c..85a383270 100644 --- a/flow360/component/simulation/unit_system.py +++ b/flow360/component/simulation/unit_system.py @@ -515,6 +515,7 @@ def get_class_object( allow_zero_component=True, allow_zero_norm=True, allow_negative_value=True, + allow_decreasing=True, length=3, ): """Get a dynamically created metaclass representing the vector""" @@ -558,6 +559,10 @@ def validate(vec_cls, value, info, *args, **kwargs): raise ValueError(f"arg '{value}' cannot have zero norm") if not vec_cls.allow_negative_value and any(item < 0 for item in value): raise ValueError(f"arg '{value}' cannot have negative value") + if not vec_cls.allow_decreasing and any( + x >= y for x, y in zip(value, value[1:]) + ): + raise ValueError(f"arg '{value}' is not strictly increasing") if vec_cls.type.has_defaults: value = _unit_inference_validator( @@ -597,6 +602,7 @@ def validate_with_info(value, info): cls_obj.allow_zero_norm = allow_zero_norm cls_obj.allow_zero_component = allow_zero_component cls_obj.allow_negative_value = allow_negative_value + cls_obj.allow_decreasing = allow_decreasing cls_obj.__get_pydantic_core_schema__ = lambda *args: __get_pydantic_core_schema__( cls_obj, *args ) @@ -780,6 +786,22 @@ def Moment(self): self, allow_zero_norm=False, allow_zero_component=False ) + @classproperty + def Range(self): + """ + Array value which accepts length 2 and is strictly increasing + """ + return self._VectorType.get_class_object(self, allow_decreasing=False, length=2) + + @classproperty + def PositiveRange(self): + """ + Range which contains strictly positive values + """ + return self._VectorType.get_class_object( + self, allow_negative_value=False, allow_decreasing=False, length=2 + ) + @classproperty def CoordinateGroupTranspose(self): """ diff --git a/flow360/component/simulation/validation/validation_context.py b/flow360/component/simulation/validation/validation_context.py index 0ad2b55ad..d2f78e6cf 100644 --- a/flow360/component/simulation/validation/validation_context.py +++ b/flow360/component/simulation/validation/validation_context.py @@ -161,6 +161,8 @@ def _get_farfield_method_(cls, param_as_dict: dict): return zone["method"] if zone["type"] == "UserDefinedFarfield": return "user-defined" + if zone["type"] == "WindTunnelFarfield": + return "wind-tunnel" if ( zone["type"] in [ @@ -185,7 +187,11 @@ def _get_farfield_domain_type_(cls, param_as_dict: dict): if not volume_zones: return None for zone in volume_zones: - if zone.get("type") in ("AutomatedFarfield", "UserDefinedFarfield"): + if zone.get("type") in ( + "AutomatedFarfield", + "UserDefinedFarfield", + "WindTunnelFarfield", + ): return zone.get("domain_type") return None diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index ffb19bdc7..790b96a98 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -8,7 +8,10 @@ MeshingParams, ModularMeshingWorkflow, ) -from flow360.component.simulation.meshing_param.volume_params import CustomZones +from flow360.component.simulation.meshing_param.volume_params import ( + CustomZones, + WindTunnelFarfield, +) from flow360.component.simulation.models.solver_numerics import NoneSolver from flow360.component.simulation.models.surface_models import ( Inflow, @@ -349,9 +352,7 @@ def _check_complete_boundary_condition_and_unknown_surface( # since we do not know the final bounding box for each surface and global model. return params - # If transformed then `_will_be_deleted_by_mesher()` will no longer be accurate - # since we do not know the final bounding box for each surface and global model. - # pylint:disable=protected-access + # pylint:disable=protected-access,duplicate-code asset_boundary_entities = [ item for item in asset_boundary_entities @@ -362,6 +363,7 @@ def _check_complete_boundary_condition_and_unknown_surface( 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, ) is False ] @@ -377,13 +379,18 @@ def _check_complete_boundary_condition_and_unknown_surface( for item in params.private_attribute_asset_cache.project_entity_info.ghost_entities if item.name != "symmetric" ] - elif farfield_method == "user-defined": + elif farfield_method in ("user-defined", "wind-tunnel"): if validation_info.will_generate_forced_symmetry_plane(): asset_boundary_entities += [ item for item in params.private_attribute_asset_cache.project_entity_info.ghost_entities if item.name == "symmetric" ] + if farfield_method == "wind-tunnel": + asset_boundary_entities += WindTunnelFarfield._get_valid_ghost_surfaces( + params.meshing.volume_zones[0].floor_type.type_name, + params.meshing.volume_zones[0].domain_type, + ) snappy_multizone = False potential_zone_zone_interfaces = set() diff --git a/tests/simulation/params/meshing_validation/test_meshing_param_validation.py b/tests/simulation/params/meshing_validation/test_meshing_param_validation.py index a4f177897..d28989ff6 100644 --- a/tests/simulation/params/meshing_validation/test_meshing_param_validation.py +++ b/tests/simulation/params/meshing_validation/test_meshing_param_validation.py @@ -20,10 +20,14 @@ AutomatedFarfield, AxisymmetricRefinement, CustomZones, + FullyMovingFloor, RotationVolume, + StaticFloor, StructuredBoxRefinement, UniformRefinement, UserDefinedFarfield, + WheelBelts, + WindTunnelFarfield, ) from flow360.component.simulation.primitives import ( AxisymmetricBody, @@ -1084,3 +1088,98 @@ def test_surface_refinement_in_gai_mesher(): with ValidationContext(SURFACE_MESH, non_gai_context): with CGS_unit_system: SurfaceRefinement(entities=Surface(name="testFace")) + + +def test_wind_tunnel_invalid_dimensions(): + with CGS_unit_system: + # invalid floors + with pytest.raises( + pd.ValidationError, + match=r"is not strictly increasing", + ): + # invalid range + _ = StaticFloor(friction_patch_x_range=(-100, -200), friction_patch_width=42) + + with pytest.raises( + pd.ValidationError, + match=r"cannot have negative value", + ): + # invalid positive range + _ = WheelBelts( + central_belt_x_range=(-200, 256), + central_belt_width=67, + front_wheel_belt_x_range=(-30, 50), + front_wheel_belt_y_range=(70, 120), + rear_wheel_belt_x_range=(260, 380), + rear_wheel_belt_y_range=(-5, 101), # here + ) + + with pytest.raises( + pd.ValidationError, + match=r"must be less than rear wheel belt minimum x", + ): + # front, rear belt x ranges overlap + _ = WheelBelts( + central_belt_x_range=(-200, 256), + central_belt_width=67, + front_wheel_belt_x_range=(-30, 263), # here + front_wheel_belt_y_range=(70, 120), + rear_wheel_belt_x_range=(260, 380), + rear_wheel_belt_y_range=(70, 120), + ) + + # invalid tunnels + with pytest.raises( + pd.ValidationError, + match=r"must be less than outlet x position", + ): + # inlet behind outlet + _ = WindTunnelFarfield( + inlet_x_position=200, outlet_x_position=182, floor_type=FullyMovingFloor() + ) + + with pytest.raises( + pd.ValidationError, + match=r"must be less than wind tunnel width", + ): + # friction patch too wide + _ = WindTunnelFarfield(width=2025, floor_type=StaticFloor(friction_patch_width=9001)) + + with pytest.raises( + pd.ValidationError, + match=r"must be greater than inlet x", + ): + # friction patch x min too small + _ = WindTunnelFarfield( + inlet_x_position=-2025, floor_type=StaticFloor(friction_patch_x_range=(-9001, 333)) + ) + + with pytest.raises( + pd.ValidationError, + match=r"must be less than half of wind tunnel width", + ): + # wheel belt y outer too large + _ = WindTunnelFarfield( + width=538, # here + floor_type=WheelBelts( + central_belt_x_range=(-200, 256), + central_belt_width=120, + front_wheel_belt_x_range=(-30, 50), + front_wheel_belt_y_range=(70, 270), # here + rear_wheel_belt_x_range=(260, 380), + rear_wheel_belt_y_range=(70, 120), + ), + ) + + # legal, despite wheel belts being ahead/behind rather than left/right of central belt + _ = WindTunnelFarfield( + width=1024, + floor_type=WheelBelts( + central_belt_x_range=(-100, 105), + central_belt_width=900.1, + front_wheel_belt_x_range=(-30, 50), + front_wheel_belt_y_range=(70, 123), + rear_wheel_belt_x_range=(260, 380), + rear_wheel_belt_y_range=(70, 120), + ), + ) diff --git a/tests/simulation/params/test_automated_farfield.py b/tests/simulation/params/test_automated_farfield.py index 11597af9b..e130df322 100644 --- a/tests/simulation/params/test_automated_farfield.py +++ b/tests/simulation/params/test_automated_farfield.py @@ -125,7 +125,9 @@ def test_automated_farfield_surface_usage(): my_farfield = AutomatedFarfield(name="my_farfield") with pytest.raises( ValueError, - match=re.escape("Can not find any valid entity of type ['Surface'] from the input."), + match=re.escape( + "Can not find any valid entity of type ['Surface', 'WindTunnelGhostSurface'] from the input." + ), ): _ = SimulationParams( meshing=MeshingParams( diff --git a/tests/simulation/translator/data/gai_windtunnel_farfield_info/simulation.json b/tests/simulation/translator/data/gai_windtunnel_farfield_info/simulation.json new file mode 100644 index 000000000..ed1b9f45f --- /dev/null +++ b/tests/simulation/translator/data/gai_windtunnel_farfield_info/simulation.json @@ -0,0 +1,1038 @@ +{ + "version": "25.8.0b5", + "unit_system": { + "name": "SI" + }, + "meshing": { + "type_name": "MeshingParams", + "refinement_factor": 1.0, + "gap_treatment_strength": 0.0, + "defaults": { + "geometry_accuracy": { + "value": 0.01, + "units": "m" + }, + "surface_edge_growth_rate": 1.2, + "boundary_layer_growth_rate": 1.2, + "boundary_layer_first_layer_thickness": { + "value": 0.0001, + "units": "m" + }, + "planar_face_tolerance": 0.001, + "surface_max_aspect_ratio": 10.0, + "surface_max_adaptation_iterations": 50, + "curvature_resolution_angle": { + "value": 15.0, + "units": "degree" + }, + "resolve_face_boundaries": false, + "preserve_thin_geometry": false, + "sealing_size": { + "value": 0.0, + "units": "m" + }, + "remove_non_manifold_faces": false + }, + "refinements": [], + "volume_zones": [ + { + "type": "WindTunnelFarfield", + "name": "Wind Tunnel Farfield", + "width": { + "value": 10.0, + "units": "m" + }, + "height": { + "value": 10.0, + "units": "m" + }, + "inlet_x_position": { + "value": -5.0, + "units": "m" + }, + "outlet_x_position": { + "value": 15.0, + "units": "m" + }, + "floor_z_position": { + "value": 0.0, + "units": "m" + }, + "floor_type": { + "type_name": "WheelBelts", + "central_belt_x_range": { + "value": [ + -1.0, + 6.0 + ], + "units": "m" + }, + "central_belt_width": { + "value": 1.2, + "units": "m" + }, + "front_wheel_belt_x_range": { + "value": [ + -0.3, + 0.5 + ], + "units": "m" + }, + "front_wheel_belt_y_range": { + "value": [ + 0.7, + 1.2 + ], + "units": "m" + }, + "rear_wheel_belt_x_range": { + "value": [ + 2.6, + 3.8 + ], + "units": "m" + }, + "rear_wheel_belt_y_range": { + "value": [ + 0.7, + 1.2 + ], + "units": "m" + } + } + } + ], + "outputs": [] + }, + "reference_geometry": { + "moment_center": { + "value": [ + 0.0, + 0.0, + 0.35 + ], + "units": "m" + }, + "moment_length": { + "value": [ + 1.0, + 1.0, + 1.0 + ], + "units": "m" + }, + "area": { + "type_name": "number", + "value": 0.0875, + "units": "m**2" + } + }, + "operating_condition": { + "type_name": "AerospaceCondition", + "private_attribute_constructor": "default", + "private_attribute_input_cache": { + "alpha": { + "value": 0.0, + "units": "degree" + }, + "beta": { + "value": 0.0, + "units": "degree" + }, + "thermal_state": { + "type_name": "ThermalState", + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "value": 288.15, + "units": "K" + }, + "density": { + "value": 1.225, + "units": "kg/m**3" + }, + "material": { + "type": "air", + "name": "air", + "dynamic_viscosity": { + "reference_viscosity": { + "value": 1.716e-05, + "units": "Pa*s" + }, + "reference_temperature": { + "value": 273.15, + "units": "K" + }, + "effective_temperature": { + "value": 110.4, + "units": "K" + } + } + } + } + }, + "alpha": { + "value": 0.0, + "units": "degree" + }, + "beta": { + "value": 0.0, + "units": "degree" + }, + "velocity_magnitude": { + "type_name": "number", + "value": 30.0, + "units": "m/s" + }, + "thermal_state": { + "type_name": "ThermalState", + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "value": 288.15, + "units": "K" + }, + "density": { + "value": 1.225, + "units": "kg/m**3" + }, + "material": { + "type": "air", + "name": "air", + "dynamic_viscosity": { + "reference_viscosity": { + "value": 1.716e-05, + "units": "Pa*s" + }, + "reference_temperature": { + "value": 273.15, + "units": "K" + }, + "effective_temperature": { + "value": 110.4, + "units": "K" + } + } + } + } + }, + "models": [ + { + "material": { + "type": "air", + "name": "air", + "dynamic_viscosity": { + "reference_viscosity": { + "value": 1.716e-05, + "units": "Pa*s" + }, + "reference_temperature": { + "value": 273.15, + "units": "K" + }, + "effective_temperature": { + "value": 110.4, + "units": "K" + } + } + }, + "initial_condition": { + "type_name": "NavierStokesInitialCondition", + "rho": "rho", + "u": "u", + "v": "v", + "w": "w", + "p": "p" + }, + "private_attribute_id": "3b994175-a93b-4f4b-83cb-d8c15341a040", + "type": "Fluid", + "navier_stokes_solver": { + "absolute_tolerance": 1e-09, + "relative_tolerance": 0.0, + "order_of_accuracy": 2, + "equation_evaluation_frequency": 1, + "linear_solver": { + "max_iterations": 50 + }, + "CFL_multiplier": 1.0, + "kappa_MUSCL": -1.0, + "numerical_dissipation_factor": 1.0, + "limit_velocity": false, + "limit_pressure_density": false, + "type_name": "Compressible", + "low_mach_preconditioner": true, + "update_jacobian_frequency": 4, + "max_force_jac_update_physical_steps": 0 + }, + "turbulence_model_solver": { + "absolute_tolerance": 1e-08, + "relative_tolerance": 0.0, + "order_of_accuracy": 2, + "equation_evaluation_frequency": 4, + "linear_solver": { + "max_iterations": 20 + }, + "CFL_multiplier": 2.0, + "type_name": "SpalartAllmaras", + "reconstruction_gradient_limiter": 0.5, + "quadratic_constitutive_relation": false, + "modeling_constants": { + "type_name": "SpalartAllmarasConsts", + "C_DES": 0.72, + "C_d": 8.0, + "C_cb1": 0.1355, + "C_cb2": 0.622, + "C_sigma": 0.6666666666666666, + "C_v1": 7.1, + "C_vonKarman": 0.41, + "C_w2": 0.3, + "C_w4": 0.21, + "C_w5": 1.5, + "C_t3": 1.2, + "C_t4": 0.5, + "C_min_rd": 10.0 + }, + "update_jacobian_frequency": 4, + "max_force_jac_update_physical_steps": 0, + "rotation_correction": false, + "low_reynolds_correction": false + }, + "transition_model_solver": { + "type_name": "None" + }, + "interface_interpolation_tolerance": 0.2 + }, + { + "type": "SlipWall", + "entities": { + "stored_entities": [ + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelLeft", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelRight", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelCeiling", + "used_by": [ + "all" + ] + } + ] + }, + "private_attribute_id": "61079dd8-fcdd-42f8-98d1-654b46208e4e", + "name": "Slip wall" + }, + { + "type": "Wall", + "entities": { + "stored_entities": [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_Curved", + "name": "cylinder.lb8.ugrid_Curved", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_Curved" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.35, + -0.12500000000000003, + -5.551115123125783e-17 + ], + [ + 0.35, + 0.12500000000000003, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_TopCap", + "name": "cylinder.lb8.ugrid_TopCap", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_TopCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.3499999999999999, + 0.12499999999999997, + 0.0 + ], + [ + 0.3499999999999999, + 0.12500000000000003, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_BottomCap", + "name": "cylinder.lb8.ugrid_BottomCap", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_BottomCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.3499999999999999, + -0.12500000000000003, + 0.0 + ], + [ + 0.3499999999999999, + -0.12499999999999997, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelFloor", + "used_by": [ + "all" + ] + } + ] + }, + "private_attribute_id": "70c1af85-cfd7-4019-8862-2664f1624e86", + "name": "Wall", + "use_wall_function": false, + "heat_spec": { + "value": { + "value": 0.0, + "units": "W/m**2" + }, + "type_name": "HeatFlux" + }, + "roughness_height": { + "value": 0.0, + "units": "m" + } + }, + { + "type": "Wall", + "entities": { + "stored_entities": [ + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelCentralBelt", + "used_by": [ + "CentralBelt", + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelFrontWheelBelt", + "used_by": [ + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelRearWheelBelt", + "used_by": [ + "WheelBelts" + ] + } + ] + }, + "private_attribute_id": "40c0d1fc-0d35-49cb-a646-c55aac5a8e80", + "name": "Wall", + "use_wall_function": true, + "velocity": { + "value": [ + 30.0, + 0.0, + 0.0 + ], + "units": "m/s" + }, + "heat_spec": { + "value": { + "value": 0.0, + "units": "W/m**2" + }, + "type_name": "HeatFlux" + }, + "roughness_height": { + "value": 0.0, + "units": "m" + } + }, + { + "type": "Freestream", + "entities": { + "stored_entities": [ + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelInlet", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelOutlet", + "used_by": [ + "all" + ] + } + ] + }, + "private_attribute_id": "4d6673a6-46b7-48a1-bc99-fbae60743f57", + "name": "Freestream" + } + ], + "time_stepping": { + "type_name": "Steady", + "max_steps": 100, + "CFL": { + "type": "adaptive", + "min": 0.1, + "max": 1000.0, + "max_relative_change": 1.0, + "convergence_limiting_factor": 0.25 + } + }, + "user_defined_fields": [], + "outputs": [ + { + "output_fields": { + "items": [ + "primitiveVars", + "Mach", + "mutRatio" + ] + }, + "private_attribute_id": "8aeb83b2-3f2e-42f1-adfb-f77d0814e6f6", + "frequency": -1, + "frequency_offset": 0, + "output_format": "paraview", + "name": "Volume output", + "output_type": "VolumeOutput" + }, + { + "output_fields": { + "items": [ + "primitiveVars", + "Cp", + "Cf", + "yPlus" + ] + }, + "private_attribute_id": "94abc649-1480-4fe7-948d-815d997d14b9", + "frequency": -1, + "frequency_offset": 0, + "output_format": "paraview", + "name": "Surface output", + "entities": { + "stored_entities": [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_Curved", + "name": "cylinder.lb8.ugrid_Curved", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_Curved" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.35, + -0.12500000000000003, + -5.551115123125783e-17 + ], + [ + 0.35, + 0.12500000000000003, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_TopCap", + "name": "cylinder.lb8.ugrid_TopCap", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_TopCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.3499999999999999, + 0.12499999999999997, + 0.0 + ], + [ + 0.3499999999999999, + 0.12500000000000003, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_BottomCap", + "name": "cylinder.lb8.ugrid_BottomCap", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_BottomCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.3499999999999999, + -0.12500000000000003, + 0.0 + ], + [ + 0.3499999999999999, + -0.12499999999999997, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelFloor", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelCentralBelt", + "used_by": [ + "CentralBelt", + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelFrontWheelBelt", + "used_by": [ + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelRearWheelBelt", + "used_by": [ + "WheelBelts" + ] + } + ] + }, + "write_single_file": false, + "output_type": "SurfaceOutput" + } + ], + "private_attribute_asset_cache": { + "project_length_unit": { + "value": 1.0, + "units": "m" + }, + "project_entity_info": { + "draft_entities": [], + "ghost_entities": [ + { + "private_attribute_entity_type_name": "GhostSphere", + "private_attribute_id": "farfield", + "name": "farfield", + "center": [ + 0, + 0, + 0 + ], + "max_radius": 35.0 + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-1", + "name": "symmetric-1", + "center": [ + 0.0, + -0.12500000000000003, + 0.35 + ], + "max_radius": 35.0, + "normal_axis": [ + 0, + -1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-2", + "name": "symmetric-2", + "center": [ + 0.0, + 0.12500000000000003, + 0.35 + ], + "max_radius": 35.0, + "normal_axis": [ + 0, + 1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric", + "name": "symmetric", + "center": [ + 0.0, + 0, + 0.35 + ], + "max_radius": 35.0, + "normal_axis": [ + 0, + 1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelInlet", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelOutlet", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelLeft", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelRight", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelCeiling", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelFloor", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelFrictionPatch", + "used_by": [ + "StaticFloor" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelCentralBelt", + "used_by": [ + "CentralBelt", + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelFrontWheelBelt", + "used_by": [ + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "name": "windTunnelRearWheelBelt", + "used_by": [ + "WheelBelts" + ] + } + ], + "type_name": "GeometryEntityInfo", + "body_ids": [ + "cylinder.lb8.ugrid" + ], + "body_attribute_names": [ + "bodyId", + "groupByFile" + ], + "grouped_bodies": [ + [ + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "cylinder.lb8.ugrid", + "name": "cylinder.lb8.ugrid", + "private_attribute_tag_key": "bodyId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid" + ], + "transformation": { + "type_name": "BodyGroupTransformation", + "origin": { + "value": [ + 0.0, + 0.0, + 0.0 + ], + "units": "m" + }, + "axis_of_rotation": [ + 1.0, + 0.0, + 0.0 + ], + "angle_of_rotation": { + "value": 0.0, + "units": "degree" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ], + "translation": { + "value": [ + 0.0, + 0.0, + 0.0 + ], + "units": "m" + } + }, + "mesh_exterior": true + } + ], + [ + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "cylinder.lb8.ugrid", + "name": "cylinder.lb8.ugrid", + "private_attribute_tag_key": "groupByFile", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid" + ], + "transformation": { + "type_name": "BodyGroupTransformation", + "origin": { + "value": [ + 0.0, + 0.0, + 0.0 + ], + "units": "m" + }, + "axis_of_rotation": [ + 1.0, + 0.0, + 0.0 + ], + "angle_of_rotation": { + "value": 0.0, + "units": "degree" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ], + "translation": { + "value": [ + 0.0, + 0.0, + 0.0 + ], + "units": "m" + } + }, + "mesh_exterior": true + } + ] + ], + "face_ids": [ + "cylinder.lb8.ugrid_BottomCap", + "cylinder.lb8.ugrid_Curved", + "cylinder.lb8.ugrid_TopCap" + ], + "face_attribute_names": [ + "groupByBodyId", + "faceId" + ], + "grouped_faces": [ + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid", + "name": "cylinder.lb8.ugrid", + "private_attribute_tag_key": "groupByBodyId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_BottomCap", + "cylinder.lb8.ugrid_Curved", + "cylinder.lb8.ugrid_TopCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.35, + -0.12500000000000003, + -5.551115123125783e-17 + ], + [ + 0.35, + 0.12500000000000003, + 0.7 + ] + ] + } + } + ], + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_Curved", + "name": "cylinder.lb8.ugrid_Curved", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_Curved" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.35, + -0.12500000000000003, + -5.551115123125783e-17 + ], + [ + 0.35, + 0.12500000000000003, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_TopCap", + "name": "cylinder.lb8.ugrid_TopCap", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_TopCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.3499999999999999, + 0.12499999999999997, + 0.0 + ], + [ + 0.3499999999999999, + 0.12500000000000003, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_BottomCap", + "name": "cylinder.lb8.ugrid_BottomCap", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_BottomCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.3499999999999999, + -0.12500000000000003, + 0.0 + ], + [ + 0.3499999999999999, + -0.12499999999999997, + 0.7 + ] + ] + } + } + ] + ], + "edge_ids": [], + "edge_attribute_names": [], + "grouped_edges": [], + "body_group_tag": "groupByFile", + "face_group_tag": "faceId", + "global_bounding_box": [ + [ + -0.35, + -0.12500000000000003, + -5.551115123125783e-17 + ], + [ + 0.35, + 0.12500000000000003, + 0.7 + ] + ], + "default_geometry_accuracy": { + "value": 0.0001, + "units": "m" + } + }, + "use_inhouse_mesher": true, + "use_geometry_AI": true + } +} \ No newline at end of file diff --git a/tests/simulation/translator/ref/Flow360_windtunnel.json b/tests/simulation/translator/ref/Flow360_windtunnel.json new file mode 100644 index 000000000..ff1a63e8c --- /dev/null +++ b/tests/simulation/translator/ref/Flow360_windtunnel.json @@ -0,0 +1,202 @@ +{ + "freestream": { + "alphaAngle": 0.0, + "betaAngle": 0.0, + "Mach": 0.08815906095303899, + "Temperature": 288.15, + "muRef": 4.292321046986505e-08 + }, + "timeStepping": { + "CFL": { + "type": "adaptive", + "min": 0.1, + "max": 1000.0, + "maxRelativeChange": 1.0, + "convergenceLimitingFactor": 0.25 + }, + "physicalSteps": 1, + "orderOfAccuracy": 2, + "maxPseudoSteps": 100, + "timeStepSize": "inf" + }, + "navierStokesSolver": { + "absoluteTolerance": 1e-09, + "relativeTolerance": 0.0, + "orderOfAccuracy": 2, + "linearSolver": { + "maxIterations": 50 + }, + "CFLMultiplier": 1.0, + "kappaMUSCL": -1.0, + "numericalDissipationFactor": 1.0, + "limitVelocity": false, + "limitPressureDensity": false, + "lowMachPreconditioner": true, + "lowMachPreconditionerThreshold": 0.08815906095303888, + "updateJacobianFrequency": 4, + "maxForceJacUpdatePhysicalSteps": 0, + "modelType": "Compressible", + "equationEvalFrequency": 1 + }, + "turbulenceModelSolver": { + "absoluteTolerance": 1e-08, + "relativeTolerance": 0.0, + "orderOfAccuracy": 2, + "linearSolver": { + "maxIterations": 20 + }, + "CFLMultiplier": 2.0, + "reconstructionGradientLimiter": 0.5, + "quadraticConstitutiveRelation": false, + "updateJacobianFrequency": 4, + "maxForceJacUpdatePhysicalSteps": 0, + "rotationCorrection": false, + "lowReynoldsCorrection": false, + "equationEvalFrequency": 4, + "modelType": "SpalartAllmaras", + "modelConstants": { + "C_DES": 0.72, + "C_d": 8.0, + "C_cb1": 0.1355, + "C_cb2": 0.622, + "C_sigma": 0.6666666666666666, + "C_v1": 7.1, + "C_vonKarman": 0.41, + "C_w2": 0.3, + "C_w4": 0.21, + "C_w5": 1.5, + "C_t3": 1.2, + "C_t4": 0.5, + "C_min_rd": 10.0 + }, + "DDES": false, + "ZDES": false, + "gridSizeForLES": "maxEdgeLength" + }, + "initialCondition": { + "type": "initialCondition", + "rho": "rho", + "u": "u", + "v": "v", + "w": "w", + "p": "p" + }, + "boundaries": { + "windTunnelLeft": { + "type": "SlipWall" + }, + "windTunnelRight": { + "type": "SlipWall" + }, + "windTunnelCeiling": { + "type": "SlipWall" + }, + "windTunnelFloor": { + "type": "NoSlipWall", + "heatFlux": 0.0, + "roughnessHeight": 0.0 + }, + "windTunnelCentralBelt": { + "type": "WallFunction", + "velocity": [ + 0.08815906095303899, + 0.0, + 0.0 + ], + "heatFlux": 0.0, + "roughnessHeight": 0.0 + }, + "windTunnelFrontWheelBelt": { + "type": "WallFunction", + "velocity": [ + 0.08815906095303899, + 0.0, + 0.0 + ], + "heatFlux": 0.0, + "roughnessHeight": 0.0 + }, + "windTunnelRearWheelBelt": { + "type": "WallFunction", + "velocity": [ + 0.08815906095303899, + 0.0, + 0.0 + ], + "heatFlux": 0.0, + "roughnessHeight": 0.0 + }, + "windTunnelInlet": { + "type": "Freestream" + }, + "windTunnelOutlet": { + "type": "Freestream" + } + }, + "volumeOutput": { + "outputFields": [ + "Mach", + "mutRatio", + "primitiveVars" + ], + "outputFormat": "paraview", + "animationFrequency": -1, + "animationFrequencyOffset": 0, + "animationFrequencyTimeAverage": -1, + "animationFrequencyTimeAverageOffset": 0, + "startAverageIntegrationStep": -1 + }, + "surfaceOutput": { + "outputFields": [], + "outputFormat": "paraview", + "animationFrequency": -1, + "animationFrequencyOffset": 0, + "surfaces": { + "windTunnelFloor": { + "outputFields": [ + "Cf", + "Cp", + "primitiveVars", + "yPlus" + ] + }, + "windTunnelCentralBelt": { + "outputFields": [ + "Cf", + "Cp", + "primitiveVars", + "yPlus" + ] + }, + "windTunnelFrontWheelBelt": { + "outputFields": [ + "Cf", + "Cp", + "primitiveVars", + "yPlus" + ] + }, + "windTunnelRearWheelBelt": { + "outputFields": [ + "Cf", + "Cp", + "primitiveVars", + "yPlus" + ] + } + }, + "writeSingleFile": false, + "animationFrequencyTimeAverage": -1, + "animationFrequencyTimeAverageOffset": 0, + "startAverageIntegrationStep": -1 + }, + "userDefinedFields": [], + "runControl": { + "shouldCheckStopCriterion": false, + "externalProcessMonitorOutput": false + }, + "usingLiquidAsMaterial": false, + "outputRescale": { + "velocityScale": 1.0 + } +} \ No newline at end of file diff --git a/tests/simulation/translator/ref/surface_meshing/gai_windtunnel.json b/tests/simulation/translator/ref/surface_meshing/gai_windtunnel.json new file mode 100644 index 000000000..79612e8e1 --- /dev/null +++ b/tests/simulation/translator/ref/surface_meshing/gai_windtunnel.json @@ -0,0 +1,321 @@ +{ + "meshing": { + "defaults": { + "surface_max_edge_length": { + "value": 0.2, + "units": "1.0*m" + }, + "curvature_resolution_angle": { + "value": 0.2617993877991494, + "units": "rad" + }, + "surface_edge_growth_rate": 1.2, + "geometry_accuracy": { + "value": 0.01, + "units": "1.0*m" + }, + "resolve_face_boundaries": false, + "preserve_thin_geometry": false, + "surface_max_aspect_ratio": 10.0, + "surface_max_adaptation_iterations": 50, + "sealing_size": { + "value": 0.0, + "units": "1.0*m" + }, + "remove_non_manifold_faces": false + }, + "refinements": [], + "volume_zones": [ + { + "type": "WindTunnelFarfield", + "name": "Wind Tunnel Farfield", + "width": { + "value": 10.0, + "units": "1.0*m" + }, + "height": { + "value": 10.0, + "units": "1.0*m" + }, + "inlet_x_position": { + "value": -5.0, + "units": "1.0*m" + }, + "outlet_x_position": { + "value": 15.0, + "units": "1.0*m" + }, + "floor_z_position": { + "value": 0.0, + "units": "1.0*m" + }, + "floor_type": { + "type_name": "WheelBelts", + "central_belt_x_range": { + "value": [ + -1.0, + 6.0 + ], + "units": "1.0*m" + }, + "central_belt_width": { + "value": 1.2, + "units": "1.0*m" + }, + "front_wheel_belt_x_range": { + "value": [ + -0.3, + 0.5 + ], + "units": "1.0*m" + }, + "front_wheel_belt_y_range": { + "value": [ + 0.7, + 1.2 + ], + "units": "1.0*m" + }, + "rear_wheel_belt_x_range": { + "value": [ + 2.6, + 3.8 + ], + "units": "1.0*m" + }, + "rear_wheel_belt_y_range": { + "value": [ + 0.7, + 1.2 + ], + "units": "1.0*m" + } + } + } + ] + }, + "private_attribute_asset_cache": { + "project_entity_info": { + "face_group_tag": "faceId", + "face_attribute_names": [ + "groupByBodyId", + "faceId" + ], + "grouped_faces": [ + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid", + "name": "cylinder.lb8.ugrid", + "private_attribute_tag_key": "groupByBodyId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_BottomCap", + "cylinder.lb8.ugrid_Curved", + "cylinder.lb8.ugrid_TopCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.35, + -0.12500000000000003, + -5.551115123125783e-17 + ], + [ + 0.35, + 0.12500000000000003, + 0.7 + ] + ] + } + } + ], + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_Curved", + "name": "cylinder.lb8.ugrid_Curved", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_Curved" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.35, + -0.12500000000000003, + -5.551115123125783e-17 + ], + [ + 0.35, + 0.12500000000000003, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_TopCap", + "name": "cylinder.lb8.ugrid_TopCap", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_TopCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.3499999999999999, + 0.12499999999999997, + 0.0 + ], + [ + 0.3499999999999999, + 0.12500000000000003, + 0.7 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cylinder.lb8.ugrid_BottomCap", + "name": "cylinder.lb8.ugrid_BottomCap", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid_BottomCap" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -0.3499999999999999, + -0.12500000000000003, + 0.0 + ], + [ + 0.3499999999999999, + -0.12499999999999997, + 0.7 + ] + ] + } + } + ] + ], + "body_group_tag": "groupByFile", + "body_attribute_names": [ + "bodyId", + "groupByFile" + ], + "grouped_bodies": [ + [ + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "cylinder.lb8.ugrid", + "name": "cylinder.lb8.ugrid", + "private_attribute_tag_key": "bodyId", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid" + ], + "transformation": { + "type_name": "BodyGroupTransformation", + "origin": { + "value": [ + 0.0, + 0.0, + 0.0 + ], + "units": "1.0*m" + }, + "axis_of_rotation": [ + 1.0, + 0.0, + 0.0 + ], + "angle_of_rotation": { + "value": 0.0, + "units": "rad" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ], + "translation": { + "value": [ + 0.0, + 0.0, + 0.0 + ], + "units": "1.0*m" + } + }, + "mesh_exterior": true + } + ], + [ + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "cylinder.lb8.ugrid", + "name": "cylinder.lb8.ugrid", + "private_attribute_tag_key": "groupByFile", + "private_attribute_sub_components": [ + "cylinder.lb8.ugrid" + ], + "transformation": { + "type_name": "BodyGroupTransformation", + "origin": { + "value": [ + 0.0, + 0.0, + 0.0 + ], + "units": "1.0*m" + }, + "axis_of_rotation": [ + 1.0, + 0.0, + 0.0 + ], + "angle_of_rotation": { + "value": 0.0, + "units": "rad" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ], + "translation": { + "value": [ + 0.0, + 0.0, + 0.0 + ], + "units": "1.0*m" + }, + "private_attribute_matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0 + ] + }, + "mesh_exterior": true + } + ] + ] + } + } +} \ No newline at end of file diff --git a/tests/simulation/translator/ref/volume_meshing/ref_param_to_json_wind_tunnel.json b/tests/simulation/translator/ref/volume_meshing/ref_param_to_json_wind_tunnel.json new file mode 100644 index 000000000..5669c0574 --- /dev/null +++ b/tests/simulation/translator/ref/volume_meshing/ref_param_to_json_wind_tunnel.json @@ -0,0 +1,12 @@ +{ + "refinementFactor": 1.0, + "farfield": { + "type": "wind-tunnel" + }, + "volume": { + "firstLayerThickness": 0.0001, + "growthRate": 1.2, + "gapTreatmentStrength": 0.0 + }, + "faces": {} +} \ No newline at end of file diff --git a/tests/simulation/translator/test_solver_translator.py b/tests/simulation/translator/test_solver_translator.py index c7ccd021a..a7e04cf48 100644 --- a/tests/simulation/translator/test_solver_translator.py +++ b/tests/simulation/translator/test_solver_translator.py @@ -103,6 +103,9 @@ from tests.simulation.translator.utils.actuator_disk_param_generator import ( actuator_disk_create_param, ) +from tests.simulation.translator.utils.analytic_windtunnel_param_generator import ( + create_windtunnel_params, +) from tests.simulation.translator.utils.CHTThreeCylinders_param_generator import ( create_conjugate_heat_transfer_param, ) @@ -1466,3 +1469,11 @@ def test_ghost_periodic(): translate_and_compare( processed_params, mesh_unit=1 * u.m, ref_json_file="Flow360_ghost_periodic.json", debug=True ) + + +def test_analytic_windtunnel(create_windtunnel_params): + translate_and_compare( + create_windtunnel_params, + mesh_unit=1 * u.m, + ref_json_file="Flow360_windtunnel.json", + ) diff --git a/tests/simulation/translator/test_surface_meshing_translator.py b/tests/simulation/translator/test_surface_meshing_translator.py index edf293217..a53e71683 100644 --- a/tests/simulation/translator/test_surface_meshing_translator.py +++ b/tests/simulation/translator/test_surface_meshing_translator.py @@ -36,6 +36,8 @@ AutomatedFarfield, CustomZones, UniformRefinement, + WheelBelts, + WindTunnelFarfield, ) from flow360.component.simulation.operating_condition.operating_condition import ( AerospaceCondition, @@ -1196,3 +1198,55 @@ def test_gai_translator_hashing_ignores_id(): assert ( hashes[0] == hashes[1] ), f"Hashes should be identical despite different UUIDs:\n Hash 1: {hashes[0]}\n Hash 2: {hashes[1]}" + + +def test_gai_analytic_wind_tunnel_farfield(): + with SI_unit_system: + wind_tunnel = WindTunnelFarfield( + width=10, + height=10, + inlet_x_position=-5, + outlet_x_position=15, + floor_z_position=0, + floor_type=WheelBelts( + central_belt_x_range=(-1, 6), + central_belt_width=1.2, + front_wheel_belt_x_range=(-0.3, 0.5), + front_wheel_belt_y_range=(0.7, 1.2), + rear_wheel_belt_x_range=(2.6, 3.8), + rear_wheel_belt_y_range=(0.7, 1.2), + ), + ) + meshing_params = MeshingParams( + defaults=MeshingDefaults( + surface_max_aspect_ratio=10, + curvature_resolution_angle=15 * u.deg, + geometry_accuracy=1e-2, + boundary_layer_first_layer_thickness=1e-4, + boundary_layer_growth_rate=1.2, + planar_face_tolerance=1e-3, + surface_max_edge_length=0.2, + ), + volume_zones=[wind_tunnel], + ) + with open( + os.path.join( + os.path.dirname(__file__), "data", "gai_windtunnel_farfield_info", "simulation.json" + ), + "r", + ) as fh: + asset_cache = AssetCache.model_validate( + json.load(fh).pop("private_attribute_asset_cache") + ) + + params = SimulationParams( + meshing=meshing_params, + operating_condition=AerospaceCondition(velocity_magnitude=30 * u.m / u.s), + private_attribute_asset_cache=asset_cache, + ) + + _translate_and_compare( + params, + 1 * u.m, + "gai_windtunnel.json", + ) diff --git a/tests/simulation/translator/test_volume_meshing_translator.py b/tests/simulation/translator/test_volume_meshing_translator.py index 66a6e4eac..9e7efd0de 100644 --- a/tests/simulation/translator/test_volume_meshing_translator.py +++ b/tests/simulation/translator/test_volume_meshing_translator.py @@ -31,6 +31,8 @@ StructuredBoxRefinement, UniformRefinement, UserDefinedFarfield, + WheelBelts, + WindTunnelFarfield, ) from flow360.component.simulation.outputs.outputs import Slice from flow360.component.simulation.primitives import ( @@ -937,3 +939,45 @@ def test_farfield_relative_size(): with open(ref_path, "r") as fh: ref_dict = json.load(fh) assert compare_values(translated, ref_dict) + + +def test_analytic_wind_tunnel_farfield(): + with SI_unit_system: + wind_tunnel = WindTunnelFarfield( + width=10, + height=10, + inlet_x_position=-5, + outlet_x_position=15, + floor_z_position=0, + floor_type=WheelBelts( + central_belt_x_range=(-1, 6), + central_belt_width=1.2, + front_wheel_belt_x_range=(-0.3, 0.5), + front_wheel_belt_y_range=(0.7, 1.2), + rear_wheel_belt_x_range=(2.6, 3.8), + rear_wheel_belt_y_range=(0.7, 1.2), + ), + ) + meshing_params = MeshingParams( + defaults=MeshingDefaults( + surface_max_aspect_ratio=10, + curvature_resolution_angle=15 * u.deg, + geometry_accuracy=1e-2, + boundary_layer_first_layer_thickness=1e-4, + boundary_layer_growth_rate=1.2, + planar_face_tolerance=1e-3, + ), + volume_zones=[wind_tunnel], + ) + param = SimulationParams(meshing=meshing_params) + + translated = get_volume_meshing_json(param, u.m) + ref_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "ref", + "volume_meshing", + "ref_param_to_json_wind_tunnel.json", + ) + with open(ref_path, "r") as fh: + ref_dict = json.load(fh) + assert compare_values(translated, ref_dict) diff --git a/tests/simulation/translator/utils/analytic_windtunnel_param_generator.py b/tests/simulation/translator/utils/analytic_windtunnel_param_generator.py new file mode 100644 index 000000000..947331a96 --- /dev/null +++ b/tests/simulation/translator/utils/analytic_windtunnel_param_generator.py @@ -0,0 +1,89 @@ +import pytest + +import flow360 as fl + +# copied from analyticWindTunnelToCase + + +@pytest.fixture +def create_windtunnel_params(): + with fl.SI_unit_system: + wind_tunnel = fl.WindTunnelFarfield( + width=10 * fl.u.m, + height=10 * fl.u.m, + inlet_x_position=-5 * fl.u.m, + outlet_x_position=15 * fl.u.m, + floor_z_position=0 * fl.u.m, + floor_type=fl.WheelBelts( + central_belt_x_range=(-1, 6) * fl.u.m, + central_belt_width=1.2 * fl.u.m, + front_wheel_belt_x_range=(-0.3, 0.5) * fl.u.m, + front_wheel_belt_y_range=(0.7, 1.2) * fl.u.m, + rear_wheel_belt_x_range=(2.6, 3.8) * fl.u.m, + rear_wheel_belt_y_range=(0.7, 1.2) * fl.u.m, + ), + ) + meshing_params = fl.MeshingParams( + defaults=fl.MeshingDefaults( + surface_max_aspect_ratio=10, + curvature_resolution_angle=15 * fl.u.deg, + geometry_accuracy=1e-2 * fl.u.m, + boundary_layer_first_layer_thickness=1e-4 * fl.u.m, + boundary_layer_growth_rate=1.2, + planar_face_tolerance=1e-3, + ), + volume_zones=[wind_tunnel], + ) + + simulation_params = fl.SimulationParams( + meshing=meshing_params, + operating_condition=fl.AerospaceCondition(velocity_magnitude=30 * fl.u.m / fl.u.s), + models=[ + fl.Fluid( + navier_stokes_solver=fl.NavierStokesSolver( + linear_solver=fl.LinearSolver(max_iterations=50), + absolute_tolerance=1e-9, + low_mach_preconditioner=True, + ), + turbulence_model_solver=fl.SpalartAllmaras(absolute_tolerance=1e-8), + ), + fl.SlipWall( + entities=[ + wind_tunnel.left, + wind_tunnel.right, + wind_tunnel.ceiling, + ] + ), + fl.Wall(entities=[wind_tunnel.floor], use_wall_function=False), + fl.Wall( + entities=[ + wind_tunnel.central_belt, + wind_tunnel.front_wheel_belts, + wind_tunnel.rear_wheel_belts, + ], + velocity=[30 * fl.u.m / fl.u.s, 0 * fl.u.m / fl.u.s, 0 * fl.u.m / fl.u.s], + use_wall_function=True, + ), + fl.Freestream(entities=[wind_tunnel.inlet, wind_tunnel.outlet]), + ], + time_stepping=fl.Steady( + CFL=fl.AdaptiveCFL(max=1000), + max_steps=100, # reduced to speed up simulation; might not converge + ), + outputs=[ + fl.VolumeOutput( + output_format="paraview", output_fields=["primitiveVars", "Mach", "mutRatio"] + ), + fl.SurfaceOutput( + entities=[ + wind_tunnel.floor, + wind_tunnel.central_belt, + wind_tunnel.front_wheel_belts, + wind_tunnel.rear_wheel_belts, + ], + output_format="paraview", + output_fields=["primitiveVars", "Cp", "Cf", "yPlus"], + ), + ], + ) + return simulation_params