"""Default settings for meshing using different meshing algorithms"""
from math import log2
from typing import Optional
import numpy as np
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.updater import (
DEFAULT_PLANAR_FACE_TOLERANCE,
DEFAULT_SLIDING_INTERFACE_TOLERANCE,
)
from flow360.component.simulation.unit_system import AngleType, LengthType
from flow360.component.simulation.validation.validation_context import (
SURFACE_MESH,
VOLUME_MESH,
ConditionalField,
ContextField,
ParamsValidationInfo,
contextual_field_validator,
)
from flow360.component.simulation.validation.validation_utils import (
check_geometry_ai_features,
)
[docs]
class MeshingDefaults(Flow360BaseModel):
"""
Default/global settings for meshing parameters.
Example
-------
>>> fl.MeshingDefaults(
... surface_max_edge_length=1*fl.u.m,
... surface_edge_growth_rate=1.2,
... curvature_resolution_angle=12*fl.u.deg,
... boundary_layer_growth_rate=1.1,
... boundary_layer_first_layer_thickness=1e-5*fl.u.m
... )
====
"""
# pylint: disable=no-member
geometry_accuracy: Optional[LengthType.Positive] = pd.Field(
None,
description="The smallest length scale that will be resolved accurately by the surface meshing process. "
"This parameter is only valid when using geometry AI."
"It can be overridden with class: ~flow360.GeometryRefinement.",
)
##:: Default surface edge settings
surface_edge_growth_rate: float = ContextField(
1.2,
ge=1,
description="Growth rate of the anisotropic layers grown from the edges."
"This can not be overridden per edge.",
context=SURFACE_MESH,
)
##:: Default boundary layer settings
boundary_layer_growth_rate: float = ContextField(
1.2,
description="Default growth rate for volume prism layers.",
ge=1,
context=VOLUME_MESH,
)
# pylint: disable=no-member
boundary_layer_first_layer_thickness: Optional[LengthType.Positive] = ConditionalField(
None,
description="Default first layer thickness for volumetric anisotropic layers."
" This can be overridden with :class:`~flow360.BoundaryLayer`.",
context=VOLUME_MESH,
) # Truly optional if all BL faces already have first_layer_thickness
number_of_boundary_layers: Optional[pd.NonNegativeInt] = pd.Field(
None,
description="Default number of volumetric anisotropic layers."
" The volume mesher will automatically calculate the required"
" no. of layers to grow the boundary layer elements to isotropic size if not specified."
" This is only supported by the beta mesher and can not be overridden per face.",
)
planar_face_tolerance: pd.NonNegativeFloat = pd.Field(
DEFAULT_PLANAR_FACE_TOLERANCE,
strict=True,
description="Tolerance used for detecting planar faces in the input surface mesh / geometry"
" that need to be remeshed, such as symmetry planes."
" This tolerance is non-dimensional, and represents a distance"
" relative to the largest dimension of the bounding box of the input surface mesh / geometry."
" This can not be overridden per face.",
)
# pylint: disable=duplicate-code
sliding_interface_tolerance: pd.NonNegativeFloat = ConditionalField(
DEFAULT_SLIDING_INTERFACE_TOLERANCE,
strict=True,
description="Tolerance used for detecting / creating curves in the input surface mesh / geometry lying on"
" sliding interfaces. This tolerance is non-dimensional, and represents a distance"
" relative to the smallest radius of all sliding interfaces specified in meshing parameters."
" This cannot be overridden per sliding interface.",
context=VOLUME_MESH,
)
##:: Default surface layer settings
surface_max_edge_length: Optional[LengthType.Positive] = ConditionalField(
None,
description="Default maximum edge length for surface cells."
" This can be overridden with :class:`~flow360.SurfaceRefinement`.",
context=SURFACE_MESH,
)
surface_max_aspect_ratio: pd.PositiveFloat = ConditionalField(
10.0,
description="Maximum aspect ratio for surface cells for the GAI surface mesher."
" This cannot be overridden per face",
context=SURFACE_MESH,
)
surface_max_adaptation_iterations: pd.NonNegativeInt = ConditionalField(
50,
description="Maximum adaptation iterations for the GAI surface mesher.",
context=SURFACE_MESH,
)
curvature_resolution_angle: AngleType.Positive = ContextField(
12 * u.deg,
description=(
"Default maximum angular deviation in degrees. This value will restrict:"
" 1. The angle between a cell’s normal and its underlying surface normal."
" 2. The angle between a line segment’s normal and its underlying curve normal."
" This can be overridden per face only when using geometry AI."
),
context=SURFACE_MESH,
)
resolve_face_boundaries: bool = pd.Field(
False,
description="Flag to specify whether boundaries between adjacent faces should be resolved "
+ "accurately during the surface meshing process using anisotropic mesh refinement. "
+ "This option is only supported when using geometry AI, and can be overridden "
+ "per face with :class:`~flow360.SurfaceRefinement`.",
)
preserve_thin_geometry: bool = pd.Field(
False,
description="Flag to specify whether thin geometry features with thickness roughly equal "
+ "to geometry_accuracy should be resolved accurately during the surface meshing process. "
+ "This option is only supported when using geometry AI, and can be overridden "
+ "per face with :class:`~flow360.GeometryRefinement`.",
)
sealing_size: LengthType.NonNegative = pd.Field(
0.0 * u.m,
description="Threshold size below which all geometry gaps are automatically closed. "
+ "This option is only supported when using geometry AI, and can be overridden "
+ "per face with :class:`~flow360.GeometryRefinement`.",
)
remove_non_manifold_faces: bool = pd.Field(
False,
description="Flag to remove non-manifold and interior faces.",
)
@contextual_field_validator("number_of_boundary_layers", mode="after")
@classmethod
def invalid_number_of_boundary_layers(cls, value, param_info: ParamsValidationInfo):
"""Ensure number of boundary layers is not specified"""
if value is not None and not param_info.is_beta_mesher:
raise ValueError("Number of boundary layers is only supported by the beta mesher.")
return value
@contextual_field_validator("geometry_accuracy", mode="after")
@classmethod
def invalid_geometry_accuracy(cls, value, param_info: ParamsValidationInfo):
"""Ensure geometry accuracy is not specified when GAI is not used"""
if value is not None and not param_info.use_geometry_AI:
raise ValueError("Geometry accuracy is only supported when geometry AI is used.")
if value is None and param_info.use_geometry_AI:
raise ValueError("Geometry accuracy is required when geometry AI is used.")
return value
@contextual_field_validator(
"surface_max_aspect_ratio",
"surface_max_adaptation_iterations",
"resolve_face_boundaries",
"preserve_thin_geometry",
"sealing_size",
"remove_non_manifold_faces",
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)
[docs]
class VolumeMeshingDefaults(Flow360BaseModel):
"""
Default/global settings for volume meshing parameters. To be used with class:`ModularMeshingWorkflow`.
"""
##:: Default boundary layer settings
boundary_layer_growth_rate: float = pd.Field(
1.2,
description="Default growth rate for volume prism layers.",
ge=1,
)
# pylint: disable=no-member
boundary_layer_first_layer_thickness: LengthType.Positive = pd.Field(
description="Default first layer thickness for volumetric anisotropic layers."
" This can be overridden with :class:`~flow360.BoundaryLayer`.",
)
number_of_boundary_layers: Optional[pd.NonNegativeInt] = pd.Field(
None,
description="Default number of volumetric anisotropic layers."
" The volume mesher will automatically calculate the required"
" no. of layers to grow the boundary layer elements to isotropic size if not specified."
" This is only supported by the beta mesher and can not be overridden per face.",
)
[docs]
class OctreeSpacing(Flow360BaseModel):
"""
Helper class for octree-based meshers. Holds the base for the octree spacing and lows calculation of levels.
"""
# pylint: disable=no-member
base_spacing: LengthType.Positive
@pd.model_validator(mode="before")
@classmethod
def _project_spacing_to_object(cls, input_data):
if isinstance(input_data, u.unyt.unyt_quantity):
return {"base_spacing": input_data}
return input_data
@pd.validate_call
def __getitem__(self, idx: int):
return self.base_spacing * (2 ** (-idx))
# pylint: disable=no-member
[docs]
@pd.validate_call
def to_level(self, spacing: LengthType.Positive):
"""
Can be used to check in what refinement level would the given spacing result
and if it is a direct match in the spacing series.
"""
level = -log2(spacing / self.base_spacing)
direct_spacing = np.isclose(level, np.round(level), atol=1e-8)
returned_level = np.round(level) if direct_spacing else np.ceil(level)
return returned_level, direct_spacing