Source code for flow360_schema.models.reference_geometry

"""Reference geometry model for simulation parameters."""

from __future__ import annotations

from typing import Annotated, Any, Literal, cast

import pydantic as pd
from pydantic import Discriminator, Tag

from flow360_schema.framework.base_model import Flow360BaseModel
from flow360_schema.framework.entity.entity_list import EntityList
from flow360_schema.framework.expression.value_or_expression import ValueOrExpression
from flow360_schema.framework.physical_dimensions import Area, Length
from flow360_schema.models.entities.surface_entities import Surface


# `computed` stays outside the recipe fields so it remains an output-only result.
[docs] class ProjectedArea(Flow360BaseModel): """Recipe for automatically computing a projected reference area.""" model_config = pd.ConfigDict(json_schema_mode_override="serialization") type_name: Literal["projected_area"] = pd.Field("projected_area", frozen=True) surfaces: EntityList[Surface] = pd.Field( description="Explicit surfaces and surface selectors used for the projected-area calculation." ) direction: Literal["X", "Y", "Z"] = pd.Field("X", description="Global projection direction.") render_quality: Literal["low", "medium", "high", "ultra"] = pd.Field( "medium", description="Raster resolution used for projected-area calculation, and therefore " "the accuracy of the result. The area counts whole pixels of a rasterized " "silhouette, so the error is proportional to pixel size: each step up halves it, " "at roughly four times the computation.", ) # Positive area preserves the reference-area constraint and uses the standard physical wire format. _computed: Area.PositiveFloat64 | None = pd.PrivateAttr(None) def __init__(self, /, **data: Any) -> None: if "computed" in data: raise TypeError("`computed` is output-only and cannot be passed to ProjectedArea(...).") super().__init__(**data) @pd.model_validator(mode="wrap") @classmethod def _deserialize_computed(cls, data: Any, handler: pd.ValidatorFunctionWrapHandler) -> Any: if not isinstance(data, dict): return handler(data) data = data.copy() computed = data.pop("computed", None) result = handler(data) if computed is not None: result._set_computed(value=computed) return result @pd.computed_field( return_type=Area.PositiveFloat64 | None, repr=False, json_schema_extra={ "readOnly": True, "description": "Latest projected-area value computed before submission in SI units.", }, ) @property def computed(self) -> Area.PositiveFloat64 | None: """Return the latest computed result, if automatic calculation has run.""" return self._computed def _set_computed(self, *, value: Area.PositiveFloat64) -> None: """Set the computed result from submission preparation code.""" self._computed = pd.TypeAdapter(Area.PositiveFloat64).validate_python(value)
[docs] def preprocess( self, *, params: Any = None, exclude: list[str] | None = None, required_by: list[str] | None = None, flow360_unit_system: Any = None, ) -> ProjectedArea: """Preserve and nondimensionalize the output-only result for translation.""" result = cast( "ProjectedArea", super().preprocess( params=params, exclude=exclude, required_by=required_by, flow360_unit_system=flow360_unit_system, ), ) if self.computed is not None: result._set_computed(value=self.computed.in_base(flow360_unit_system)) return result
@classmethod def __get_pydantic_json_schema__(cls, core_schema: Any, handler: Any) -> dict[str, Any]: """Keep the output-only computed property optional in generated schemas.""" schema = handler(core_schema) required = schema.get("required") if isinstance(required, list): schema["required"] = [field for field in required if field != "computed"] return schema
def _reference_area_discriminator(value: Any) -> str: """Route projected-area recipes separately from values and expressions.""" if isinstance(value, ProjectedArea): return "projected_area" if isinstance(value, dict) and (value.get("type_name") or value.get("typeName")) == "projected_area": return "projected_area" return "value_or_expression" ReferenceArea = Annotated[ Annotated[ProjectedArea, Tag("projected_area")] | Annotated[ValueOrExpression[Area.PositiveFloat64], Tag("value_or_expression")], Discriminator(_reference_area_discriminator), ]
[docs] class ReferenceGeometry(Flow360BaseModel): """ :class:`ReferenceGeometry` class contains all geometrical related reference values. Example ------- >>> ReferenceGeometry( ... moment_center=(1, 2, 1) * u.m, ... moment_length=(1, 1, 1) * u.m, ... area=1.5 * u.m**2 ... ) >>> ReferenceGeometry( ... moment_center=(1, 2, 1) * u.m, ... moment_length=1 * u.m, ... area=1.5 * u.m**2 ... ) # Equivalent to above ==== """ moment_center: Length.Vector3 | None = pd.Field(None, description="The x, y, z coordinate of moment center.") moment_length: Length.PositiveFloat64 | Length.PositiveVector3 | None = pd.Field( None, description="The x, y, z component-wise moment reference lengths." ) area: ReferenceArea | None = pd.Field(None, description="The reference area of the geometry.") private_attribute_area_settings: dict | None = pd.Field( None, description="Deprecated Web user interface state retained only when reading legacy simulation data.", )
[docs] @classmethod def fill_defaults(cls, ref, params): # type: ignore[override] """Return a new ReferenceGeometry with defaults filled using SimulationParams.""" base_length_unit = params.base_length if ref is None: ref = cls() area = ref.area if area is None: area = 1.0 * (base_length_unit**2) moment_center = ref.moment_center if moment_center is None: moment_center = (0, 0, 0) * base_length_unit moment_length = ref.moment_length if moment_length is None: moment_length = (1.0, 1.0, 1.0) * base_length_unit return cls( area=area, moment_center=moment_center, moment_length=moment_length, private_attribute_area_settings=ref.private_attribute_area_settings, )