Skip to content
Merged
2 changes: 2 additions & 0 deletions flow360/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
Cylinder,
ReferenceGeometry,
SeedpointVolume,
Sphere,
)
from flow360.component.simulation.run_control.run_control import RunControl
from flow360.component.simulation.run_control.stopping_criterion import (
Expand Down Expand Up @@ -248,6 +249,7 @@
"ReferenceGeometry",
"CustomVolume",
"Cylinder",
"Sphere",
"AxisymmetricBody",
"AerospaceCondition",
"ThermalState",
Expand Down
42 changes: 42 additions & 0 deletions flow360/component/simulation/entity_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from flow360.component.simulation.framework.entity_utils import generate_uuid
from flow360.component.simulation.unit_system import AngleType, LengthType
from flow360.component.types import Axis
from flow360.exceptions import Flow360ValueError


def rotation_matrix_from_axis_and_angle(axis, angle):
Expand Down Expand Up @@ -163,6 +164,47 @@ def _is_uniform_scale(matrix: np.ndarray, rtol: float = 1e-5) -> bool:
return np.allclose(scale_factors, scale_factors[0], rtol=rtol)


def _validate_uniform_scale_and_transform_center(
matrix: np.ndarray, center, entity_name: str
) -> tuple:
"""
Common transformation logic for volume primitives that require uniform scaling.

Validates that the transformation matrix has uniform scaling, extracts the scale factor,
and transforms the center point.

Args:
matrix: 3x4 transformation matrix
center: The center point (LengthType.Point) to transform
entity_name: Name of the entity type (e.g., "Sphere", "Cylinder") for error messages

Returns:
Tuple of (new_center, uniform_scale) where:
- new_center: Transformed center point with same type and units as input
- uniform_scale: The uniform scale factor extracted from the matrix

Raises:
Flow360ValueError: If the matrix has non-uniform scaling
"""
# Validate uniform scaling
if not _is_uniform_scale(matrix):
scale_factors = _extract_scale_from_matrix(matrix)
raise Flow360ValueError(
f"{entity_name} only supports uniform scaling. "
f"Detected scale factors: {scale_factors}"
)

# Extract uniform scale factor
uniform_scale = _extract_scale_from_matrix(matrix)[0]

# Transform center
center_array = np.asarray(center.value)
new_center_array = _transform_point(center_array, matrix)
new_center = type(center)(new_center_array, center.units)

return new_center, uniform_scale


def _extract_rotation_matrix(matrix: np.ndarray) -> np.ndarray:
"""
Extract the pure rotation matrix from a 3x4 transformation matrix,
Expand Down
2 changes: 2 additions & 0 deletions flow360/component/simulation/framework/entity_materializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
MirroredSurface,
SeedpointVolume,
SnappyBody,
Sphere,
Surface,
WindTunnelGhostSurface,
)
Expand All @@ -62,6 +63,7 @@
"AxisymmetricBody": AxisymmetricBody,
"Box": Box,
"Cylinder": Cylinder,
"Sphere": Sphere,
"ImportedSurface": ImportedSurface,
"GhostSurface": GhostSurface,
"GhostSphere": GhostSphere,
Expand Down
150 changes: 115 additions & 35 deletions flow360/component/simulation/meshing_param/volume_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

# pylint: disable=too-many-lines

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

import pydantic as pd
Expand All @@ -23,6 +22,7 @@
GhostSurface,
MirroredSurface,
SeedpointVolume,
Sphere,
Surface,
WindTunnelGhostSurface,
)
Expand Down Expand Up @@ -162,20 +162,7 @@ def _validate_only_in_beta_mesher(self, param_info: ParamsValidationInfo):
raise ValueError("`StructuredBoxRefinement` is only supported with the beta mesher.")


class AxisymmetricRefinementBase(Flow360BaseModel, metaclass=ABCMeta):
"""Base class for all refinements that requires spacing in axial, radial and circumferential directions."""

# pylint: disable=no-member
spacing_axial: LengthType.Positive = pd.Field(description="Spacing along the axial direction.")
spacing_radial: LengthType.Positive = pd.Field(
description="Spacing along the radial direction."
)
spacing_circumferential: LengthType.Positive = pd.Field(
description="Spacing along the circumferential direction."
)


class AxisymmetricRefinement(AxisymmetricRefinementBase):
class AxisymmetricRefinement(Flow360BaseModel):
"""
- The mesh inside the :class:`AxisymmetricRefinement` is semi-structured.
- The :class:`AxisymmetricRefinement` cannot enclose/intersect with other objects.
Expand All @@ -202,22 +189,36 @@ class AxisymmetricRefinement(AxisymmetricRefinementBase):
"AxisymmetricRefinement", frozen=True
)
entities: EntityList[Cylinder] = pd.Field()
# pylint: disable=no-member
spacing_axial: LengthType.Positive = pd.Field(description="Spacing along the axial direction.")
spacing_radial: LengthType.Positive = pd.Field(
description="Spacing along the radial direction."
)
spacing_circumferential: LengthType.Positive = pd.Field(
description="Spacing along the circumferential direction."
)


class RotationVolume(AxisymmetricRefinementBase):
class RotationVolume(Flow360BaseModel):
"""
Creates a rotation volume mesh using cylindrical or axisymmetric body entities.
Creates a rotation volume mesh using cylindrical, axisymmetric body, or sphere entities.

- The mesh on :class:`RotationVolume` is guaranteed to be concentric.
- The :class:`RotationVolume` is designed to enclose other objects, but it can't intersect with other objects.
- Users can create a donut-shaped :class:`RotationVolume` and put their stationary centerbody in the middle.
- This type of volume zone can be used to generate volume zones compatible with :class:`~flow360.Rotation` model.
- Supports both :class:`Cylinder` and :class:`AxisymmetricBody` entities for defining the rotation volume geometry.
- Supports :class:`Cylinder`, :class:`AxisymmetricBody`, and :class:`Sphere` entities
for defining the rotation volume geometry.

.. note::
The deprecated :class:`RotationCylinder` class is maintained for backward compatibility
but only accepts :class:`Cylinder` entities. New code should use :class:`RotationVolume`.

.. note::
For :class:`Sphere` entities, only `spacing_circumferential` is required (uniform spacing on the surface).
For :class:`Cylinder` and :class:`AxisymmetricBody` entities, `spacing_axial`, `spacing_radial`,
and `spacing_circumferential` are all required.

Example
-------
Using a Cylinder entity:
Expand All @@ -240,6 +241,14 @@ class RotationVolume(AxisymmetricRefinementBase):
... entities=axisymmetric_body
... )

Using a Sphere entity (spherical sliding interface):

>>> fl.RotationVolume(
... name="RotationSphere",
... spacing_circumferential=0.3*fl.u.m,
... entities=sphere
... )

With enclosed entities:

>>> fl.RotationVolume(
Expand All @@ -258,16 +267,17 @@ class RotationVolume(AxisymmetricRefinementBase):

type: Literal["RotationVolume"] = pd.Field("RotationVolume", frozen=True)
name: Optional[str] = pd.Field("Rotation Volume", description="Name to display in the GUI.")
entities: EntityList[Cylinder, AxisymmetricBody] = pd.Field()
entities: EntityList[Cylinder, AxisymmetricBody, Sphere] = pd.Field()
enclosed_entities: Optional[
EntityList[Cylinder, Surface, MirroredSurface, AxisymmetricBody, Box]
EntityList[Cylinder, Surface, MirroredSurface, AxisymmetricBody, Box, Sphere]
] = pd.Field(
None,
description=(
"Entities enclosed by :class:`RotationVolume`. "
"Can be :class:`~flow360.Surface` and/or other :class:`~flow360.Cylinder`"
"and/or other :class:`~flow360.AxisymmetricBody`"
"and/or other :class:`~flow360.Box`"
"and/or other :class:`~flow360.Sphere`"
),
)
stationary_enclosed_entities: Optional[EntityList[Surface, MirroredSurface]] = pd.Field(
Expand All @@ -277,19 +287,32 @@ class RotationVolume(AxisymmetricRefinementBase):
"(excluded from rotation)."
),
)
# pylint: disable=no-member
spacing_axial: Optional[LengthType.Positive] = pd.Field(
None, description="Spacing along the axial direction."
)
spacing_radial: Optional[LengthType.Positive] = pd.Field(
None, description="Spacing along the radial direction."
)
# This is actually a required field for all of Sphere, Cylinder, AxisymmetricBody entity
# RotationVolumes, but making this not Optional causes validation to be triggered in pydantic
# vs in validator below, giving different error messages than what we want.
# Use of validation_default=False messes up schemas.
spacing_circumferential: Optional[LengthType.Positive] = pd.Field(
Comment thread
shreyas-flex marked this conversation as resolved.
None, description="Spacing along the circumferential direction."
)
Comment thread
shreyas-flex marked this conversation as resolved.

@contextual_field_validator("entities", mode="after")
@classmethod
def _validate_single_instance_in_entity_list(cls, values):
def _validate_single_instance_in_entity_list(cls, values, param_info: ParamsValidationInfo):
"""
[CAPABILITY-LIMITATION]
Multiple instances in the entities is not allowed.
Because enclosed_entities will almost certain be different.
`enclosed_entities` is planned to be auto_populated in the future.
Only single instance is allowed in entities for each `RotationVolume`.
"""
# pylint: disable=protected-access
# Note: Should be fine without expansion since we only allow Draft entities here.
if len(values.stored_entities) > 1:
# But using expand_entity_list for consistency and future-proofing.
expanded_entities = param_info.expand_entity_list(values)
if len(expanded_entities) > 1:
raise ValueError(
"Only single instance is allowed in entities for each `RotationVolume`."
)
Expand All @@ -305,11 +328,11 @@ def _validate_cylinder_name_length(cls, values, param_info: ParamsValidationInfo
"""
if param_info.is_beta_mesher:
return values
# Note: Should be fine without expansion since we only allow Draft entities here.

expanded_entities = param_info.expand_entity_list(values)
cgns_max_zone_name_length = 32
max_cylinder_name_length = cgns_max_zone_name_length - len("rotatingBlock-")
for entity in values.stored_entities:
for entity in expanded_entities:
if isinstance(entity, Cylinder) and len(entity.name) > max_cylinder_name_length:
raise ValueError(
f"The name ({entity.name}) of `Cylinder` entity in `RotationVolume` "
Expand All @@ -319,11 +342,9 @@ def _validate_cylinder_name_length(cls, values, param_info: ParamsValidationInfo

@contextual_field_validator("enclosed_entities", mode="after")
@classmethod
def _validate_enclosed_box_only_in_beta_mesher(cls, values, param_info: ParamsValidationInfo):
def _validate_enclosed_entities_beta_mesher_only(cls, values, param_info: ParamsValidationInfo):
"""
Check the name length for the cylinder entities due to the 32-character
limitation of all data structure names and labels in CGNS format.
The current prefix is 'rotatingBlock-' with 14 characters.
Ensure that Box and Sphere entities in enclosed_entities are only used with the beta mesher.
"""
if values is None:
return values
Expand All @@ -336,23 +357,32 @@ def _validate_enclosed_box_only_in_beta_mesher(cls, values, param_info: ParamsVa
raise ValueError(
"`Box` entity in `RotationVolume.enclosed_entities` is only supported with the beta mesher."
)
if isinstance(entity, Sphere):
raise ValueError(
"`Sphere` entity in `RotationVolume.enclosed_entities` is only supported with the beta mesher."
)

return values

@contextual_field_validator("entities", mode="after")
@classmethod
def _validate_axisymmetric_only_in_beta_mesher(cls, values, param_info: ParamsValidationInfo):
def _validate_entities_beta_mesher_only(cls, values, param_info: ParamsValidationInfo):
"""
Ensure that axisymmetric RotationVolumes are only processed with the beta mesher.
Ensure that AxisymmetricBody and Sphere entities are only used with the beta mesher.
Comment thread
shreyas-flex marked this conversation as resolved.
"""
if param_info.is_beta_mesher:
return values

for entity in values.stored_entities:
expanded_entities = param_info.expand_entity_list(values)
for entity in expanded_entities:
if isinstance(entity, AxisymmetricBody):
raise ValueError(
"`AxisymmetricBody` entity for `RotationVolume` is only supported with the beta mesher."
)
if isinstance(entity, Sphere):
raise ValueError(
"`Sphere` entity for `RotationVolume` is only supported with the beta mesher."
)
return values

@contextual_field_validator("enclosed_entities", mode="after")
Expand Down Expand Up @@ -409,6 +439,56 @@ def _validate_stationary_enclosed_entities_subset(self, param_info: ParamsValida

return self

@contextual_model_validator(mode="after")
def _validate_spacing_requirements_by_entity_type(self, param_info: ParamsValidationInfo):
"""
Validate spacing requirements based on entity type:
- Sphere: only spacing_circumferential is required; spacing_axial and spacing_radial must not be specified
- Cylinder/AxisymmetricBody: all three spacings are required
"""
# Check if entity is a Sphere
# pylint: disable=no-member
expanded_entities = param_info.expand_entity_list(self.entities)
has_sphere = any(isinstance(entity, Sphere) for entity in expanded_entities)
has_cylinder_or_axisymmetric = any(
isinstance(entity, (Cylinder, AxisymmetricBody)) for entity in expanded_entities
)

if has_sphere:
if self.spacing_circumferential is None:
raise ValueError(
"`spacing_circumferential` is required for `Sphere` entities in `RotationVolume`."
)
if self.spacing_axial is not None:
raise ValueError(
"`spacing_axial` must not be specified for `Sphere` entities. "
"Sphere uses only `spacing_circumferential` for uniform surface spacing."
)
if self.spacing_radial is not None:
raise ValueError(
"`spacing_radial` must not be specified for `Sphere` entities. "
"Sphere uses only `spacing_circumferential` for uniform surface spacing."
)

if has_cylinder_or_axisymmetric:
if self.spacing_axial is None:
raise ValueError(
"`spacing_axial` is required for `Cylinder` or `AxisymmetricBody` entities "
"in `RotationVolume`."
)
if self.spacing_radial is None:
raise ValueError(
"`spacing_radial` is required for `Cylinder` or `AxisymmetricBody` entities "
"in `RotationVolume`."
)
if self.spacing_circumferential is None:
Comment thread
shreyas-flex marked this conversation as resolved.
raise ValueError(
"`spacing_circumferential` is required for `Cylinder` or `AxisymmetricBody` "
"entities in `RotationVolume`."
)

return self

Comment thread
shreyas-flex marked this conversation as resolved.

@deprecated(
"The `RotationCylinder` class is deprecated! Use `RotationVolume`,"
Expand Down
Loading
Loading