"""Face based meshing parameters for meshing."""
from typing import Literal
import pydantic as pd
from flow360_schema.framework.base_model import Flow360BaseModel
from flow360_schema.framework.entity.entity_list import EntityList
from flow360_schema.framework.physical_dimensions import Angle, Length
from flow360_schema.models.entities.surface_entities import (
GhostCircularPlane,
GhostSurface,
MirroredSurface,
Surface,
WindTunnelGhostSurface,
)
from flow360_schema.models.simulation.validation.validation_context import (
ParamsValidationInfo,
contextual_field_validator,
contextual_model_validator,
)
from flow360_schema.models.simulation.validation.validation_utils import (
check_deleted_surface_in_entity_list,
check_geometry_ai_features,
check_ghost_surface_usage_policy_for_face_refinements,
)
[docs]
class SurfaceRefinement(Flow360BaseModel):
"""
Setting for refining surface elements for given `Surface`.
Example
-------
>>> fl.SurfaceRefinement(
... faces=[geometry["face1"], geometry["face2"]],
... max_edge_length=0.001*fl.u.m
... )
====
"""
name: str | None = pd.Field("Surface refinement")
refinement_type: Literal["SurfaceRefinement"] = pd.Field("SurfaceRefinement", frozen=True)
entities: EntityList[Surface, MirroredSurface, WindTunnelGhostSurface, GhostSurface, GhostCircularPlane] = pd.Field(
alias="faces"
)
max_edge_length: Length.PositiveFloat64 | None = pd.Field(None, description="Maximum edge length of surface cells.")
curvature_resolution_angle: Angle.PositiveFloat64 | None = pd.Field(
None,
description=(
"Default maximum angular deviation in degrees. "
"This value will restrict the angle between a cell’s normal and its underlying surface normal."
),
)
resolve_face_boundaries: bool | None = pd.Field(
None,
description="Flag to specify whether boundaries between adjacent faces should be resolved "
+ "accurately during the surface meshing process using anisotropic mesh refinement.",
)
@contextual_field_validator("entities", mode="after")
@classmethod
def ensure_surface_existence(cls, value, param_info: ParamsValidationInfo):
"""Ensure all boundaries will be present after mesher"""
expanded = param_info.expand_entity_list(value)
check_ghost_surface_usage_policy_for_face_refinements(
expanded, feature_name="SurfaceRefinement", param_info=param_info
)
check_deleted_surface_in_entity_list(expanded, param_info)
return value
@contextual_field_validator("curvature_resolution_angle", mode="after")
@classmethod
def ensure_geometry_ai_or_beta_mesher(cls, value, param_info: ParamsValidationInfo):
"""Ensure curvature resolution angle is specified only when beta mesher or geometry AI is used"""
if value is not None and not (param_info.is_beta_mesher or param_info.use_geometry_AI):
raise ValueError(
"curvature_resolution_angle is only supported by the beta mesher or when geometry AI is enabled"
)
return value
@contextual_field_validator("resolve_face_boundaries", mode="after")
@classmethod
def ensure_geometry_ai_features(cls, value, info, param_info: ParamsValidationInfo):
"""Validate that the feature is only used when Geometry AI is enabled."""
return check_geometry_ai_features(cls, value, info, param_info)
@pd.model_validator(mode="after")
def require_at_least_one_setting(self):
"""Ensure that at least one of max_edge_length, curvature_resolution_angle,
or resolve_face_boundaries is specified for SurfaceRefinement.
"""
if (
self.max_edge_length is None
and self.curvature_resolution_angle is None
and self.resolve_face_boundaries is None
):
raise ValueError(
"SurfaceRefinement requires at least one of 'max_edge_length', "
"'curvature_resolution_angle', or 'resolve_face_boundaries' to be specified."
)
return self
[docs]
class GeometryRefinement(Flow360BaseModel):
"""
Setting for refining surface elements for given `Surface`.
Example
-------
>>> fl.GeometryRefinement(
... faces=[geometry["face1"], geometry["face2"]],
... geometry_accuracy=0.001*fl.u.m
... )
====
"""
name: str | None = pd.Field("Geometry refinement")
refinement_type: Literal["GeometryRefinement"] = pd.Field("GeometryRefinement", frozen=True)
entities: EntityList[Surface, MirroredSurface, WindTunnelGhostSurface] = pd.Field(alias="faces")
geometry_accuracy: Length.PositiveFloat64 | None = pd.Field(
None,
description="The smallest length scale that will be resolved accurately by the surface meshing process. ",
)
preserve_thin_geometry: bool | None = pd.Field(
None,
description="Flag to specify whether thin geometry features with thickness roughly equal "
+ "to geometry_accuracy should be resolved accurately during the surface meshing process.",
)
sealing_size: Length.NonNegativeFloat64 | None = pd.Field(
None,
description="Threshold size below which all geometry gaps are automatically closed.",
)
min_passage_size: Length.PositiveFloat64 | None = pd.Field(
None,
description="Minimum passage size that hidden geometry removal can resolve for this face group. "
"Internal regions connected by thin passages smaller than this size may not be detected. "
"If not specified, the value is derived from geometry_accuracy and sealing_size.",
)
# Note: No checking on deleted surfaces since geometry accuracy on deleted surface does impact the volume mesh.
@contextual_model_validator(mode="after")
def ensure_geometry_ai(self, param_info: ParamsValidationInfo):
"""Ensure feature is only activated with geometry AI enabled."""
if not param_info.use_geometry_AI:
raise ValueError("GeometryRefinement is only supported by geometry AI.")
return self
[docs]
class PassiveSpacing(Flow360BaseModel):
"""
Passively control the mesh spacing either through adjacent `Surface`'s meshing
setting or doing nothing to change existing surface mesh at all.
Example
-------
>>> fl.PassiveSpacing(
... faces=[geometry["face1"], geometry["face2"]],
... type="projected"
... )
====
"""
name: str | None = pd.Field("Passive spacing")
type: Literal["projected", "unchanged"] = pd.Field(
description="""
1. When set to *projected*, turn off anisotropic layers growing for this `Surface`.
Project the anisotropic spacing from the neighboring volumes to this face.
2. When set to *unchanged*, turn off anisotropic layers growing for this `Surface`.
The surface mesh will remain unaltered when populating the volume mesh.
"""
)
refinement_type: Literal["PassiveSpacing"] = pd.Field("PassiveSpacing", frozen=True)
entities: EntityList[Surface, MirroredSurface, WindTunnelGhostSurface, GhostSurface, GhostCircularPlane] = pd.Field(
alias="faces"
)
@contextual_field_validator("entities", mode="after")
@classmethod
def ensure_surface_existence(cls, value, param_info: ParamsValidationInfo):
"""Ensure all boundaries will be present after mesher"""
expanded = param_info.expand_entity_list(value)
check_ghost_surface_usage_policy_for_face_refinements(
expanded, feature_name="PassiveSpacing", param_info=param_info
)
check_deleted_surface_in_entity_list(expanded, param_info)
return value
[docs]
class BoundaryLayer(Flow360BaseModel):
"""
Setting for growing anisotropic layers orthogonal to the specified `Surface` (s).
Example
-------
>>> fl.BoundaryLayer(
... faces=[geometry["face1"], geometry["face2"]],
... first_layer_thickness=1e-5,
... growth_rate=1.15
... )
====
"""
name: str | None = pd.Field("Boundary layer refinement")
refinement_type: Literal["BoundaryLayer"] = pd.Field("BoundaryLayer", frozen=True)
entities: EntityList[Surface, MirroredSurface, WindTunnelGhostSurface] = pd.Field(alias="faces")
first_layer_thickness: Length.PositiveFloat64 | None = pd.Field(
None,
description="First layer thickness for volumetric anisotropic layers grown from given `Surface` (s).",
)
growth_rate: float | None = pd.Field(
None,
ge=1,
description="Growth rate for volume prism layers for given `Surface` (s)."
" Supported only by the beta mesher.",
)
@contextual_field_validator("entities", mode="after")
@classmethod
def ensure_surface_existence(cls, value, param_info: ParamsValidationInfo):
"""Ensure all boundaries will be present after mesher"""
expanded = param_info.expand_entity_list(value)
check_deleted_surface_in_entity_list(expanded, param_info)
return value
@contextual_field_validator("growth_rate", mode="after")
@classmethod
def invalid_growth_rate(cls, value, param_info: ParamsValidationInfo):
"""Ensure growth rate per face is not specified"""
if value is not None and not param_info.is_beta_mesher:
raise ValueError("Growth rate per face is only supported by the beta mesher.")
return value
@contextual_field_validator("first_layer_thickness", mode="after")
@classmethod
def require_first_layer_thickness(cls, value, param_info: ParamsValidationInfo):
"""Verify first layer thickness is specified"""
if value is None and not param_info.is_beta_mesher:
raise ValueError("First layer thickness is required.")
return value