Source code for flow360.component.simulation.models.surface_models

"""
Contains basically only boundary conditons for now. In future we can add new models like 2D equations.
"""

from abc import ABCMeta
from typing import Annotated, Dict, Literal, Optional, Union

import pydantic as pd

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.framework.entity_utils import generate_uuid
from flow360.component.simulation.framework.expressions import StringExpression
from flow360.component.simulation.framework.single_attribute_base import (
    SingleAttributeModel,
)
from flow360.component.simulation.framework.unique_list import UniqueItemList
from flow360.component.simulation.models.turbulence_quantities import (
    TurbulenceQuantitiesType,
)
from flow360.component.simulation.operating_condition.operating_condition import (
    VelocityVectorType,
)
from flow360.component.simulation.primitives import (
    GhostCircularPlane,
    GhostSphere,
    GhostSurface,
    GhostSurfacePair,
    MirroredSurface,
    Surface,
    SurfacePair,
    WindTunnelGhostSurface,
)
from flow360.component.simulation.unit_system import (
    AbsoluteTemperatureType,
    AngularVelocityType,
    HeatFluxType,
    InverseAreaType,
    InverseLengthType,
    LengthType,
    MassFlowRateType,
    PressureType,
)
from flow360.component.simulation.validation.validation_context import (
    ParamsValidationInfo,
    contextual_field_validator,
)
from flow360.component.simulation.validation.validation_utils import (
    check_deleted_surface_pair,
    validate_entity_list_surface_existence,
)

# pylint: disable=fixme
# TODO: Warning: Pydantic V1 import
from flow360.component.types import Axis


class BoundaryBase(Flow360BaseModel, metaclass=ABCMeta):
    """Boundary base"""

    type: str = pd.Field()
    entities: EntityList[Surface, MirroredSurface] = pd.Field(
        alias="surfaces",
        description="List of boundaries with boundary condition imposed.",
    )
    private_attribute_id: str = pd.Field(default_factory=generate_uuid, frozen=True)

    @contextual_field_validator("entities", mode="after")
    @classmethod
    def ensure_surface_existence(cls, value, param_info: ParamsValidationInfo):
        """Ensure all boundaries will be present after mesher"""
        # pylint: disable=fixme
        # TODO: This should have been moved to EntityListAllowingGhost?
        return validate_entity_list_surface_existence(value, param_info)


class BoundaryBaseWithTurbulenceQuantities(BoundaryBase, metaclass=ABCMeta):
    """Boundary base with turbulence quantities"""

    turbulence_quantities: Optional[TurbulenceQuantitiesType] = pd.Field(
        None,
        description="The turbulence related quantities definition."
        + "See :func:`TurbulenceQuantities` documentation.",
    )


[docs] class HeatFlux(SingleAttributeModel): """ :class:`HeatFlux` class to specify the heat flux for `Wall` boundary condition via :py:attr:`Wall.heat_spec`. Example ------- >>> fl.HeatFlux(value = 1.0 * fl.u.W/fl.u.m**2) ==== """ type_name: Literal["HeatFlux"] = pd.Field("HeatFlux", frozen=True) value: Union[StringExpression, HeatFluxType] = pd.Field(description="The heat flux value.")
[docs] class Temperature(SingleAttributeModel): """ :class:`Temperature` class to specify the temperature for `Wall` or `Inflow` boundary condition via :py:attr:`Wall.heat_spec`/ :py:attr:`Inflow.spec`. Example ------- >>> fl.Temperature(value = 350 * fl.u.K) ==== """ type_name: Literal["Temperature"] = pd.Field("Temperature", frozen=True) # pylint: disable=no-member value: Union[StringExpression, AbsoluteTemperatureType] = pd.Field( description="The temperature value." )
[docs] class TotalPressure(Flow360BaseModel): """ :class:`TotalPressure` class to specify the total pressure for `Inflow` boundary condition via :py:attr:`Inflow.spec`. Example ------- - Using a constant value: >>> fl.TotalPressure( ... value = 1.04e6 * fl.u.Pa, ... ) - Using an expression (nondimensionalized by operating condition pressure): >>> fl.TotalPressure( ... value = "pow(1.0+0.2*pow(0.1*(1.0-y*y),2.0),1.4/0.4)", ... ) ==== """ type_name: Literal["TotalPressure"] = pd.Field("TotalPressure", frozen=True) # pylint: disable=no-member value: Union[StringExpression, PressureType.Positive] = pd.Field( description="The total pressure value. When a string expression is supplied the value" + " needs to nondimensionalized by the pressure defined in `operating_condition`." )
[docs] class Pressure(SingleAttributeModel): """ :class:`Pressure` class to specify the static pressure for `Outflow` boundary condition via :py:attr:`Outflow.spec`. Example ------- >>> fl.Pressure(value = 1.01e6 * fl.u.Pa) ==== """ type_name: Literal["Pressure"] = pd.Field("Pressure", frozen=True) # pylint: disable=no-member value: PressureType.Positive = pd.Field(description="The static pressure value.")
[docs] class SlaterPorousBleed(Flow360BaseModel): """ :class:`SlaterPorousBleed` is a no-slip wall model which prescribes a normal velocity at the surface as a function of the surface pressure and density according to the model of John Slater. Example ------- - Specify a static pressure of 1.01e6 Pascals at the slater bleed boundary, and set the porosity of the surface to 0.4 (40%). >>> fl.SlaterPorousBleed(static_pressure=1.01e6 * fl.u.Pa, porosity=0.4, activation_step=200) ==== """ type_name: Literal["SlaterPorousBleed"] = pd.Field("SlaterPorousBleed", frozen=True) # pylint: disable=no-member static_pressure: PressureType.Positive = pd.Field(description="The static pressure value.") porosity: float = pd.Field(gt=0, le=1, description="The porosity of the bleed region.") activation_step: Optional[pd.PositiveInt] = pd.Field( None, description="Pseudo step at which to start applying the SlaterPorousBleedModel." )
[docs] class MassFlowRate(Flow360BaseModel): """ :class:`MassFlowRate` class to specify the mass flow rate for `Inflow` or `Outflow` boundary condition via :py:attr:`Inflow.spec`/:py:attr:`Outflow.spec`. Example ------- >>> fl.MassFlowRate( ... value = 123 * fl.u.lb / fl.u.s, ... ramp_steps = 100, ... ) ==== """ type_name: Literal["MassFlowRate"] = pd.Field("MassFlowRate", frozen=True) # pylint: disable=no-member value: MassFlowRateType.NonNegative = pd.Field(description="The mass flow rate.") ramp_steps: Optional[pd.PositiveInt] = pd.Field( None, description="Number of pseudo steps before reaching :py:attr:`MassFlowRate.value` within 1 physical step.", )
[docs] class Supersonic(Flow360BaseModel): """ :class:`Supersonic` class to specify the supersonic conditions for `Inflow`. Example ------- >>> fl.Supersonic( ... total_pressure = 7.90e6 * fl.u.Pa, ... static_pressure = 1.01e6 * fl.u.Pa, ... ) """ type_name: Literal["Supersonic"] = pd.Field("Supersonic", frozen=True) # pylint: disable=no-member total_pressure: PressureType.Positive = pd.Field(description="The total pressure.") static_pressure: PressureType.Positive = pd.Field(description="The static pressure.")
[docs] class Mach(SingleAttributeModel): """ :class:`Mach` class to specify Mach number for the `Inflow` boundary condition via :py:attr:`Inflow.spec`. Example ------- >>> fl.Mach(value = 0.5) ==== """ type_name: Literal["Mach"] = pd.Field("Mach", frozen=True) value: pd.NonNegativeFloat = pd.Field(description="The Mach number.")
[docs] class Translational(Flow360BaseModel): """ :class:`Translational` class to specify translational periodic boundary condition via :py:attr:`Periodic.spec`. """ type_name: Literal["Translational"] = pd.Field("Translational", frozen=True)
[docs] class Rotational(Flow360BaseModel): """ :class:`Rotational` class to specify rotational periodic boundary condition via :py:attr:`Periodic.spec`. """ type_name: Literal["Rotational"] = pd.Field("Rotational", frozen=True) # pylint: disable=fixme # TODO: Maybe we need more precision when serializeing this one? axis_of_rotation: Optional[Axis] = pd.Field(None)
[docs] class WallRotation(Flow360BaseModel): """ :class:`WallRotation` class to specify the rotational velocity model for the `Wall` boundary condition. The wall rotation model prescribes a rotational motion at the wall by defining a center of rotation, an axis about which the wall rotates, and an angular velocity. This model can be used to simulate rotating components or surfaces in a flow simulation. Example ------- >>> fl.Wall( ... entities=volume_mesh["fluid/wall"], ... velocity=fl.WallRotation( ... axis=(0, 0, 1), ... center=(1, 2, 3) * u.m, ... angular_velocity=100 * u.rpm ... ), ... use_wall_function=True, ... ) ==== """ # pylint: disable=no-member center: LengthType.Point = pd.Field(description="The center of rotation") axis: Axis = pd.Field(description="The axis of rotation.") angular_velocity: AngularVelocityType = pd.Field("The value of the angular velocity.") type_name: Literal["WallRotation"] = pd.Field("WallRotation", frozen=True) private_attribute_circle_mode: Optional[dict] = pd.Field(None)
########################################## ############# Surface models ############# ########################################## WallVelocityModelTypes = Annotated[ Union[SlaterPorousBleed, WallRotation], pd.Field(discriminator="type_name") ]
[docs] class Wall(BoundaryBase): """ :class:`Wall` class defines the wall boundary condition based on the inputs. Refer :ref:`here <wall_formulations>` for formulation details. Example ------- - :code:`Wall` with wall function and prescribed velocity: >>> fl.Wall( ... entities=geometry["wall_function"], ... velocity = ["min(0.2, 0.2 + 0.2*y/0.5)", "0", "0.1*y/0.5"], ... use_wall_function=True, ... ) >>> fl.Wall( ... entities=volume_mesh["8"], ... velocity=WallRotation( ... axis=(0, 0, 1), ... center=(1, 2, 3) * u.m, ... angular_velocity=100 * u.rpm ... ), ... use_wall_function=True, ... ) - Define isothermal wall boundary condition on entities with the naming pattern :code:`"fluid/isothermal-*"`: >>> fl.Wall( ... entities=volume_mesh["fluid/isothermal-*"], ... heat_spec=fl.Temperature(350 * fl.u.K), ... ) - Define isoflux wall boundary condition on entities with the naming pattern :code:`"solid/isoflux-*"`: >>> fl.Wall( ... entities=volume_mesh["solid/isoflux-*"], ... heat_spec=fl.HeatFlux(1.0 * fl.u.W/fl.u.m**2), ... ) - Define Slater no-slip bleed model on entities with the naming pattern :code:`"fluid/SlaterBoundary-*"`: >>> fl.Wall( ... entities=volume_mesh["fluid/SlaterBoundary-*"], ... velocity=fl.SlaterPorousBleed( ... static_pressure=1.01e6 * fl.u.Pa, porosity=0.4, activation_step=200 ... ), ... ) - Define roughness height on entities with the naming pattern :code:`"fluid/Roughness-*"`: >>> fl.Wall( ... entities=volume_mesh["fluid/Roughness-*"], ... roughness_height=0.1 * fl.u.mm, ... ) ==== """ name: Optional[str] = pd.Field("Wall", description="Name of the `Wall` boundary condition.") type: Literal["Wall"] = pd.Field("Wall", frozen=True) use_wall_function: bool = pd.Field( False, description="Specify if use wall functions to estimate the velocity field " + "close to the solid boundaries.", ) velocity: Optional[Union[WallVelocityModelTypes, VelocityVectorType]] = pd.Field( None, description="Prescribe a velocity or the velocity model on the wall." ) # pylint: disable=no-member heat_spec: Union[HeatFlux, Temperature] = pd.Field( HeatFlux(0 * u.W / u.m**2), discriminator="type_name", description="Specify the heat flux or temperature at the `Wall` boundary.", ) roughness_height: LengthType.NonNegative = pd.Field( 0 * u.m, description="Equivalent sand grain roughness height. Available only to `Fluid` zone boundaries.", ) private_attribute_dict: Optional[Dict] = pd.Field(None) entities: EntityList[Surface, MirroredSurface, 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""" if self.use_wall_function is False: return self if isinstance(self.velocity, SlaterPorousBleed): raise ValueError( f"Using `{type(self.velocity).__name__}` with wall function is not supported currently." ) return self @contextual_field_validator("heat_spec", mode="after") @classmethod def _ensure_adiabatic_wall_for_liquid(cls, value, param_info: ParamsValidationInfo): """Allow only adiabatic wall when liquid operating condition is used""" if param_info.using_liquid_as_material is False: return value if isinstance(value, HeatFlux) and value.value == 0 * u.W / u.m**2: return value raise ValueError("Only adiabatic wall is allowed when using liquid as simulation material.") @contextual_field_validator("velocity", mode="after") @classmethod def _disable_expression_for_liquid(cls, value, param_info: ParamsValidationInfo): if param_info.using_liquid_as_material is False: return value if isinstance(value, tuple): if ( isinstance(value[0], str) and isinstance(value[1], str) and isinstance(value[2], str) ): raise ValueError( "Expression cannot be used when using liquid as simulation material." ) return value
[docs] class Freestream(BoundaryBaseWithTurbulenceQuantities): """ :class:`Freestream` defines the freestream boundary condition. Example ------- - Define freestream boundary condition with velocity expression and boundaries from the volume mesh: >>> fl.Freestream( ... surfaces=[volume_mesh["blk-1/freestream-part1"], ... volume_mesh["blk-1/freestream-part2"]], ... velocity = ["min(0.2, 0.2 + 0.2*y/0.5)", "0", "0.1*y/0.5"] ... ) - Define freestream boundary condition with turbulence quantities and automated farfield: >>> auto_farfield = fl.AutomatedFarfield() ... fl.Freestream( ... entities=[auto_farfield.farfield], ... turbulence_quantities= fl.TurbulenceQuantities( ... modified_viscosity_ratio=10, ... ) ... ) ==== """ name: Optional[str] = pd.Field( "Freestream", description="Name of the `Freestream` boundary condition." ) type: Literal["Freestream"] = pd.Field("Freestream", frozen=True) velocity: Optional[VelocityVectorType] = pd.Field( None, description="The default values are set according to the " + ":py:attr:`AerospaceCondition.alpha` and :py:attr:`AerospaceCondition.beta` angles. " + "Optionally, an expression for each of the velocity components can be specified.", ) entities: EntityList[ Surface, MirroredSurface, GhostSurface, WindTunnelGhostSurface, GhostSphere, GhostCircularPlane, ] = pd.Field( # pylint: disable=duplicate-code alias="surfaces", description="List of boundaries with the `Freestream` boundary condition imposed.", ) @contextual_field_validator("velocity", mode="after") @classmethod def _disable_expression_for_liquid(cls, value, param_info: ParamsValidationInfo): if param_info.using_liquid_as_material is False: return value if isinstance(value, tuple): if ( isinstance(value[0], str) and isinstance(value[1], str) and isinstance(value[2], str) ): raise ValueError( "Expression cannot be used when using liquid as simulation material." ) return value
[docs] class Outflow(BoundaryBase): """ :class:`Outflow` defines the outflow boundary condition based on the input :py:attr:`spec`. Example ------- - Define outflow boundary condition with pressure: >>> fl.Outflow( ... surfaces=volume_mesh["fluid/outlet"], ... spec=fl.Pressure(value = 0.99e6 * fl.u.Pa) ... ) - Define outflow boundary condition with Mach number: >>> fl.Outflow( ... surfaces=volume_mesh["fluid/outlet"], ... spec=fl.Mach(value = 0.2) ... ) - Define outflow boundary condition with mass flow rate: >>> fl.Outflow( ... surfaces=volume_mesh["fluid/outlet"], ... spec=fl.MassFlowRate(value = 123 * fl.u.lb / fl.u.s) ... ) ==== """ name: Optional[str] = pd.Field( "Outflow", description="Name of the `Outflow` boundary condition." ) type: Literal["Outflow"] = pd.Field("Outflow", frozen=True) spec: Union[Pressure, MassFlowRate, Mach] = pd.Field( discriminator="type_name", description="Specify the static pressure, mass flow rate, or Mach number parameters at" + " the `Outflow` boundary.", ) entities: EntityList[Surface, MirroredSurface, WindTunnelGhostSurface] = pd.Field( alias="surfaces", description="List of boundaries with the `Outflow` boundary condition imposed.", )
[docs] class Inflow(BoundaryBaseWithTurbulenceQuantities): """ :class:`Inflow` defines the inflow boundary condition based on the input :py:attr:`spec`. Example ------- - Define inflow boundary condition with pressure: >>> fl.Inflow( ... entities=[geometry["inflow"]], ... total_temperature=300 * fl.u.K, ... spec=fl.TotalPressure( ... value = 1.028e6 * fl.u.Pa, ... ), ... velocity_direction = (1, 0, 0), ... ) - Define inflow boundary condition with mass flow rate: >>> fl.Inflow( ... entities=[volume_mesh["fluid/inflow"]], ... total_temperature=300 * fl.u.K, ... spec=fl.MassFlowRate( ... value = 123 * fl.u.lb / fl.u.s, ... ramp_steps = 10, ... ), ... velocity_direction = (1, 0, 0), ... ) - Define inflow boundary condition with turbulence quantities: >>> fl.Inflow( ... entities=[volume_mesh["fluid/inflow"]], ... turbulence_quantities=fl.TurbulenceQuantities( ... turbulent_kinetic_energy=2.312e-3 * fl.u.m **2 / fl.u.s**2, ... specific_dissipation_rate= 1020 / fl.u.s, ... ) ... ) - Define inflow boundary condition with expressions for spatially varying total temperature and total pressure: >>> fl.Inflow( ... entities=[volumeMesh["fluid/inflow"]], ... total_temperature="1.0+0.2*pow(0.1*(1.0-y*y),2.0)", ... velocity_direction=(1.0, 0.0, 0.0), ... spec=fl.TotalPressure( ... value="pow(1.0+0.2*pow(0.1*(1.0-y*y),2.0),1.4/0.4)", ... ), ... ) ==== """ name: Optional[str] = pd.Field("Inflow", description="Name of the `Inflow` boundary condition.") type: Literal["Inflow"] = pd.Field("Inflow", frozen=True) # pylint: disable=no-member total_temperature: Union[StringExpression, AbsoluteTemperatureType] = pd.Field( description="Specify the total temperature at the `Inflow` boundary." + " When a string expression is supplied the value" + " needs to nondimensionalized by the temperature defined in `operating_condition`." ) spec: Union[TotalPressure, MassFlowRate, Supersonic] = pd.Field( discriminator="type_name", description="Specify additional conditions at the `Inflow` boundary.", ) velocity_direction: Optional[Axis] = pd.Field( None, description="Direction of the incoming flow. Must be a unit vector pointing " + "into the volume. If unspecified, the direction will be normal to the surface.", ) entities: EntityList[Surface, MirroredSurface, WindTunnelGhostSurface] = pd.Field( alias="surfaces", description="List of boundaries with the `Inflow` boundary condition imposed.", )
[docs] class SlipWall(BoundaryBase): """:class:`SlipWall` class defines the :code:`SlipWall` boundary condition. Example ------- Define :code:`SlipWall` boundary condition for entities with the naming pattern: :code:`"*/slipWall"` in the volume mesh. >>> fl.SlipWall(entities=volume_mesh["*/slipWall"] - Define :code:`SlipWall` boundary condition with automated farfield symmetry plane boundaries: >>> auto_farfield = fl.AutomatedFarfield() >>> fl.SlipWall( ... entities=[auto_farfield.symmetry_planes], ... turbulence_quantities= fl.TurbulenceQuantities( ... modified_viscosity_ratio=10, ... ) ... ) ==== """ name: Optional[str] = pd.Field( "Slip wall", description="Name of the `SlipWall` boundary condition." ) type: Literal["SlipWall"] = pd.Field("SlipWall", frozen=True) entities: EntityList[ Surface, MirroredSurface, GhostSurface, WindTunnelGhostSurface, GhostCircularPlane ] = pd.Field( alias="surfaces", description="List of boundaries with the :code:`SlipWall` boundary condition imposed.", )
[docs] class SymmetryPlane(BoundaryBase): """ :class:`SymmetryPlane` defines the symmetric boundary condition. It is similar to :class:`SlipWall`, but the normal gradient of scalar quantities are forced to be zero on the symmetry plane. **Only planar surfaces are supported.** Example ------- >>> fl.SymmetryPlane(entities=volume_mesh["fluid/symmetry"]) - Define `SymmetryPlane` boundary condition with automated farfield symmetry plane boundaries: >>> auto_farfield = fl.AutomatedFarfield() >>> fl.SymmetryPlane( ... entities=[auto_farfield.symmetry_planes], ... ) ==== """ name: Optional[str] = pd.Field( "Symmetry", description="Name of the `SymmetryPlane` boundary condition." ) type: Literal["SymmetryPlane"] = pd.Field("SymmetryPlane", frozen=True) entities: EntityList[Surface, MirroredSurface, GhostSurface, GhostCircularPlane] = pd.Field( alias="surfaces", description="List of boundaries with the `SymmetryPlane` boundary condition imposed.", )
[docs] class Periodic(Flow360BaseModel): """ :class:`Periodic` defines the translational or rotational periodic boundary condition. Example ------- - Define a translationally periodic boundary condition using :class:`Translational`: >>> fl.Periodic( ... surface_pairs=[ ... (volume_mesh["VOLUME/BOTTOM"], volume_mesh["VOLUME/TOP"]), ... (volume_mesh["VOLUME/RIGHT"], volume_mesh["VOLUME/LEFT"]), ... ], ... spec=fl.Translational(), ... ) - Define a rotationally periodic boundary condition using :class:`Rotational`: >>> fl.Periodic( ... surface_pairs=[(volume_mesh["VOLUME/PERIODIC-1"], ... volume_mesh["VOLUME/PERIODIC-2"])], ... spec=fl.Rotational() ... ) ==== """ name: Optional[str] = pd.Field( "Periodic", description="Name of the `Periodic` boundary condition." ) type: Literal["Periodic"] = pd.Field("Periodic", frozen=True) entity_pairs: UniqueItemList[Union[SurfacePair, GhostSurfacePair]] = pd.Field( alias="surface_pairs", description="List of matching pairs of :class:`~flow360.Surface` or `~flow360.GhostSurface`. ", ) spec: Union[Translational, Rotational] = pd.Field( discriminator="type_name", description="Define the type of periodic boundary condition (translational/rotational) " + "via :class:`Translational`/:class:`Rotational`.", ) private_attribute_id: str = pd.Field(default_factory=generate_uuid, frozen=True) @contextual_field_validator("entity_pairs", mode="after") @classmethod def ensure_surface_existence(cls, value, param_info: ParamsValidationInfo): """Ensure all boundaries will be present after mesher""" for surface_pair in value.items: check_deleted_surface_pair(surface_pair, param_info) return value @contextual_field_validator("entity_pairs", mode="after") @classmethod def _ensure_quasi_3d_periodic_when_using_ghost_surface( cls, value, param_info: ParamsValidationInfo ): """ When using ghost surface pairs, ensure the farfield type is quasi-3d-periodic. """ for surface_pair in value.items: if isinstance(surface_pair, GhostSurfacePair): if param_info.farfield_method != "quasi-3d-periodic": raise ValueError( "Farfield type must be 'quasi-3d-periodic' when using GhostSurfacePair." ) return value
[docs] class PorousJump(Flow360BaseModel): """ :class:`PorousJump` defines the Porous Jump boundary condition. Example ------- Define a porous jump condition: >>> fl.PorousJump( ... surface_pairs=[ ... (volume_mesh["blk-1/Interface-blk-2"], volume_mesh["blk-2/Interface-blk-1"]), ... (volume_mesh["blk-1/Interface-blk-3"], volume_mesh["blk-3/Interface-blk-1"]), ... ], ... darcy_coefficient = 1e6 / fl.u.m **2, ... forchheimer_coefficient = 1 / fl.u.m, ... thickness = 1 * fl.u.m, ... ) ==== """ name: Optional[str] = pd.Field( "PorousJump", description="Name of the `PorousJump` boundary condition." ) type: Literal["PorousJump"] = pd.Field("PorousJump", frozen=True) entity_pairs: UniqueItemList[SurfacePair] = pd.Field( alias="surface_pairs", description="List of matching pairs of :class:`~flow360.Surface`. " ) darcy_coefficient: InverseAreaType = pd.Field( description="Darcy coefficient of the porous media model which determines the scaling of the " + "viscous loss term. The value defines the coefficient for the axis normal " + "to the surface." ) forchheimer_coefficient: InverseLengthType = pd.Field( description="Forchheimer coefficient of the porous media model which determines " + "the scaling of the inertial loss term." ) thickness: LengthType = pd.Field( description="Thickness of the thin porous media on the surface" ) private_attribute_id: str = pd.Field(default_factory=generate_uuid, frozen=True) @contextual_field_validator("entity_pairs", mode="after") @classmethod def ensure_surface_existence(cls, value, param_info: ParamsValidationInfo): """Ensure all boundaries will be present after mesher and all entities are surfaces""" def _is_cross_custom_volume_interface(surface1, surface2) -> bool: """Check if two surfaces belong to different CustomVolumes' boundaries.""" surface1_id = surface1.private_attribute_id surface2_id = surface2.private_attribute_id cv_names_for_surface1 = set() cv_names_for_surface2 = set() for cv_name, cv_info in param_info.to_be_generated_custom_volumes.items(): boundary_ids = cv_info.get("boundary_surface_ids", set()) if surface1_id in boundary_ids: cv_names_for_surface1.add(cv_name) if surface2_id in boundary_ids: cv_names_for_surface2.add(cv_name) # Both surfaces must belong to at least one CustomVolume, # and they must not share any common CustomVolume return ( bool(cv_names_for_surface1) and bool(cv_names_for_surface2) and cv_names_for_surface1.isdisjoint(cv_names_for_surface2) ) for surface_pair in value.items: check_deleted_surface_pair(surface_pair, param_info) surface1, surface2 = surface_pair.pair # Skip interface check for cross-CustomVolume boundaries (will become interface after meshing) if _is_cross_custom_volume_interface(surface1, surface2): continue for surface in surface_pair.pair: if not surface.private_attribute_is_interface: raise ValueError(f"Boundary `{surface.name}` is not an interface") return value
SurfaceModelTypes = Union[ Wall, SlipWall, Freestream, Outflow, Inflow, Periodic, SymmetryPlane, PorousJump, ]