Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 91 additions & 32 deletions flow360/component/simulation/meshing_param/meshing_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,74 @@
from flow360.component.simulation.validation.validation_utils import (
check_geometry_ai_features,
)
from flow360.log import log


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 _reject_plain_value(cls, input_data):
if isinstance(input_data, u.unyt.unyt_quantity):
raise ValueError(
"Passing a plain dimensional value to OctreeSpacing is not supported. "
"Use OctreeSpacing(base_spacing=<value>) instead."
)
return input_data

@pd.validate_call
def __getitem__(self, idx: int):
return self.base_spacing * (2 ** (-idx))

# pylint: disable=no-member
@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

# pylint: disable=no-member
@pd.validate_call
def check_spacing(self, spacing: LengthType.Positive, location: str):
"""Warn if the given spacing does not align with the octree series."""
lvl, close = self.to_level(spacing)
if not close:
spacing_unit = spacing.units
closest_spacing = self[lvl]
msg = (
f"The spacing of {spacing:.4g} specified in {location} will be cast "
f"to the first lower refinement in the octree series "
f"({closest_spacing.to(spacing_unit):.4g})."
)
log.warning(msg)


def set_default_octree_spacing(octree_spacing, param_info: ParamsValidationInfo):
"""Shared logic for defaulting octree_spacing to 1 * project_length_unit."""
if octree_spacing is not None:
return octree_spacing
if param_info.project_length_unit is None:
add_validation_warning(
"No project length unit found; `octree_spacing` will not be set automatically. "
"Octree spacing validation will be skipped."
)
return octree_spacing

# pylint: disable=no-member
project_length = 1 * LengthType.validate(param_info.project_length_unit)
return OctreeSpacing(base_spacing=project_length)


class MeshingDefaults(Flow360BaseModel):
Expand Down Expand Up @@ -130,8 +198,8 @@ class MeshingDefaults(Flow360BaseModel):
12 * u.deg,
description=(
"Default maximum angular deviation in degrees. This value will restrict:"
" 1. The angle between a cells normal and its underlying surface normal."
" 2. The angle between a line segments normal and its underlying curve normal."
" 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,
Expand Down Expand Up @@ -183,6 +251,12 @@ class MeshingDefaults(Flow360BaseModel):
+ "This only affects beta mesher.",
)

octree_spacing: Optional[OctreeSpacing] = pd.Field(
None,
description="Octree spacing configuration for volume meshing. "
"If specified, this will be used to control the base spacing for octree-based meshers.",
)

@pd.model_validator(mode="before")
@classmethod
def remove_deprecated_arguments(cls, value):
Expand Down Expand Up @@ -250,6 +324,12 @@ def ensure_geometry_ai_features(cls, value, info, param_info: ParamsValidationIn
"""Validate that the feature is only used when Geometry AI is enabled."""
return check_geometry_ai_features(cls, value, info, param_info)

@contextual_field_validator("octree_spacing", mode="after")
@classmethod
def _set_default_octree_spacing(cls, octree_spacing, param_info: ParamsValidationInfo):
"""Set default octree_spacing to 1 * project_length_unit when not specified."""
return set_default_octree_spacing(octree_spacing, param_info)

@pd.model_validator(mode="after")
def validate_min_passage_size_requires_remove_hidden_geometry(self):
"""Ensure min_passage_size is only specified when remove_hidden_geometry is True."""
Expand Down Expand Up @@ -285,35 +365,14 @@ class VolumeMeshingDefaults(Flow360BaseModel):
" This is only supported by the beta mesher and can not be overridden per face.",
)

octree_spacing: Optional[OctreeSpacing] = pd.Field(
None,
description="Octree spacing configuration for volume meshing. "
"If specified, this will be used to control the base spacing for octree-based meshers.",
)

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")
@contextual_field_validator("octree_spacing", mode="after")
@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
@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
def _set_default_octree_spacing(cls, octree_spacing, param_info: ParamsValidationInfo):
"""Set default octree_spacing to 1 * project_length_unit when not specified."""
return set_default_octree_spacing(octree_spacing, param_info)
45 changes: 45 additions & 0 deletions flow360/component/simulation/meshing_param/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@
SURFACE_MESH,
VOLUME_MESH,
ContextField,
ParamsValidationInfo,
add_validation_warning,
contextual_field_validator,
contextual_model_validator,
)
from flow360.component.simulation.validation.validation_utils import EntityUsageMap
from flow360.log import log

RefinementTypes = Annotated[
Union[
Expand Down Expand Up @@ -334,6 +336,27 @@ def _check_no_reused_volume_entities(self) -> Self:

return self

@contextual_model_validator(mode="after")
def _check_sizing_against_octree_series(self, param_info: ParamsValidationInfo):
"""Validate that UniformRefinement spacings align with the octree series."""
if not param_info.is_beta_mesher:
return self
if self.defaults.octree_spacing is None: # pylint: disable=no-member
log.warning(
"No `octree_spacing` configured in `%s`; "
"octree spacing validation for UniformRefinement will be skipped.",
type(self.defaults).__name__,
)
return self

if self.refinements is not None:
for refinement in self.refinements: # pylint: disable=not-an-iterable
if isinstance(refinement, UniformRefinement):
self.defaults.octree_spacing.check_spacing( # pylint: disable=no-member
refinement.spacing, type(refinement).__name__
)
return self

@contextual_model_validator(mode="after")
def _warn_min_passage_size_without_remove_hidden_geometry(self) -> Self:
"""Warn when GeometryRefinement specifies min_passage_size but remove_hidden_geometry is disabled."""
Expand Down Expand Up @@ -413,6 +436,28 @@ class VolumeMeshingParams(Flow360BaseModel):
" This cannot be overridden per sliding interface.",
)

@contextual_model_validator(mode="after")
def _check_sizing_against_octree_series(self, param_info: ParamsValidationInfo):
"""Validate that UniformRefinement spacings align with the octree series."""
if not param_info.is_beta_mesher:
return self
if self.defaults.octree_spacing is None: # pylint: disable=no-member
log.warning(
"No `octree_spacing` configured in `%s`; "
"octree spacing validation for UniformRefinement will be skipped.",
type(self.defaults).__name__,
)
return self

if self.refinements is not None:
for refinement in self.refinements: # pylint: disable=not-an-iterable
if isinstance(refinement, UniformRefinement):
self.defaults.octree_spacing.check_spacing( # pylint: disable=no-member
refinement.spacing, type(refinement).__name__
)

return self


SurfaceMeshingParams = Annotated[
Union[snappy.SurfaceMeshingParams], pd.Field(discriminator="type_name")
Expand Down
30 changes: 20 additions & 10 deletions flow360/component/simulation/meshing_param/snappy/snappy_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,17 @@ class SurfaceMeshingParams(Flow360BaseModel):
castellated_mesh_controls: CastellatedMeshControls = pd.Field(CastellatedMeshControls())
smooth_controls: Union[SmoothControls, Literal[False]] = pd.Field(SmoothControls())
refinements: Optional[List[SnappySurfaceRefinementTypes]] = pd.Field(None)
base_spacing: Optional[OctreeSpacing] = pd.Field(None)
octree_spacing: Optional[OctreeSpacing] = pd.Field(None, validation_alias="base_spacing")

@pd.model_validator(mode="before")
@classmethod
def _warn_base_spacing_deprecated(cls, data):
if isinstance(data, dict) and "base_spacing" in data:
log.warning(
"`base_spacing` has been renamed to `octree_spacing`. "
"Please update your code. `base_spacing` will be removed in a future release."
)
return data

@pd.model_validator(mode="after")
def _check_body_refinements_w_defaults(self):
Expand Down Expand Up @@ -134,15 +144,15 @@ def _check_uniform_refinement_entities(self):
@pd.model_validator(mode="after")
def _check_sizing_against_octree_series(self):

if self.base_spacing is None:
if self.octree_spacing is None:
return self

def check_spacing(spacing, location):
# pylint: disable=no-member
lvl, close = self.base_spacing.to_level(spacing)
lvl, close = self.octree_spacing.to_level(spacing)
spacing_unit = spacing.units
if not close:
closest_spacing = self.base_spacing[lvl]
closest_spacing = self.octree_spacing[lvl]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snappy local check_spacing duplicates new OctreeSpacing.check_spacing method

Low Severity

The local check_spacing function inside snappy's _check_sizing_against_octree_series is functionally identical to the newly introduced OctreeSpacing.check_spacing method. The non-snappy validators (MeshingParams, VolumeMeshingParams) already call self.defaults.octree_spacing.check_spacing(...), but the snappy code was not refactored to use it. This duplication means a future bug fix in OctreeSpacing.check_spacing won't propagate to the snappy path.

Additional Locations (1)

Fix in Cursor Fix in Web

msg = f"The spacing of {spacing:.4g} specified in {location} will be cast to the first lower refinement"
msg += f" in the octree series ({closest_spacing.to(spacing_unit):.4g})."
log.warning(msg)
Expand Down Expand Up @@ -172,12 +182,12 @@ def check_spacing(spacing, location):

return self

@contextual_field_validator("base_spacing", mode="after")
@contextual_field_validator("octree_spacing", mode="after")
@classmethod
def _set_default_base_spacing(cls, base_spacing, param_info: ParamsValidationInfo):
if (base_spacing is not None) or (param_info.project_length_unit is None):
return base_spacing
def _set_default_octree_spacing(cls, octree_spacing, param_info: ParamsValidationInfo):
if (octree_spacing is not None) or (param_info.project_length_unit is None):
return octree_spacing

# pylint: disable=no-member
base_spacing = 1 * LengthType.validate(param_info.project_length_unit)
return OctreeSpacing(base_spacing=base_spacing)
project_length = 1 * LengthType.validate(param_info.project_length_unit)
return OctreeSpacing(base_spacing=project_length)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snappy ignores shared set_default_octree_spacing, missing validation warning

Low Severity

The shared set_default_octree_spacing function was introduced with a docstring saying "Shared logic" and is used by both MeshingDefaults and VolumeMeshingDefaults. However, snappy's _set_default_octree_spacing still has its own inline implementation that silently returns None when project_length_unit is missing, skipping the add_validation_warning call present in the shared function. This means snappy users don't get the "octree_spacing will not be set automatically" warning that non-snappy users receive.

Additional Locations (1)

Fix in Cursor Fix in Web

Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def snappy_mesher_json(input_params: SimulationParams):
translated = {}
surface_meshing_params = input_params.meshing.surface_meshing
# spacing system
spacing_system: OctreeSpacing = surface_meshing_params.base_spacing
spacing_system: OctreeSpacing = surface_meshing_params.octree_spacing
Comment thread
cursor[bot] marked this conversation as resolved.

# extract geometry information in body: {patch0, ...} format
bodies = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,14 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units):
["meshing", "volume_zones"],
)

if (
input_params.private_attribute_asset_cache.use_inhouse_mesher
and defaults.octree_spacing is not None
):
translated["farfield"][
"octreeBaseSpacing"
] = defaults.octree_spacing.base_spacing.value.item()

##:: Step 3: Get volumetric global settings
translated["volume"] = {}

Expand Down
Loading
Loading