Skip to content

Commit

Permalink
[SCFD-3088] Add endpoint for unit system conversion (#632) (#643)
Browse files Browse the repository at this point in the history
Added PointArray to draft entities

Changed interface for change_unit_system

Allowing also available levels to be None
  • Loading branch information
benflexcompute authored Jan 7, 2025
1 parent a6977fc commit f6cdae6
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 39 deletions.
8 changes: 6 additions & 2 deletions flow360/component/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
)
from flow360.component.resource_base import Flow360Resource
from flow360.component.simulation.entity_info import GeometryEntityInfo
from flow360.component.simulation.outputs.output_entities import Point, Slice
from flow360.component.simulation.outputs.output_entities import (
Point,
PointArray,
Slice,
)
from flow360.component.simulation.primitives import Box, Cylinder, Edge, Surface
from flow360.component.simulation.simulation_params import SimulationParams
from flow360.component.simulation.unit_system import LengthType
Expand Down Expand Up @@ -70,7 +74,7 @@ def _get_tag(entity_registry, entity_type: Union[type[Surface], type[Edge]]):

entity_registry = params.used_entity_registry
# Creating draft entities
for draft_type in [Box, Cylinder, Point, Slice]:
for draft_type in [Box, Cylinder, Point, PointArray, Slice]:
draft_entities = entity_registry.find_by_type(draft_type)
for draft_entity in draft_entities:
if draft_entity not in entity_info.draft_entities:
Expand Down
8 changes: 6 additions & 2 deletions flow360/component/simulation/entity_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import pydantic as pd

from flow360.component.simulation.framework.entity_registry import EntityRegistry
from flow360.component.simulation.outputs.output_entities import Point, Slice
from flow360.component.simulation.outputs.output_entities import (
Point,
PointArray,
Slice,
)
from flow360.component.simulation.primitives import (
Box,
Cylinder,
Expand All @@ -18,7 +22,7 @@
)

DraftEntityTypes = Annotated[
Union[Box, Cylinder, Point, Slice],
Union[Box, Cylinder, Point, PointArray, Slice],
pd.Field(discriminator="private_attribute_entity_type_name"),
]

Expand Down
118 changes: 97 additions & 21 deletions flow360/component/simulation/framework/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,10 @@
from pydantic import ConfigDict
from pydantic._internal._decorators import Decorator, FieldValidatorDecoratorInfo
from pydantic_core import InitErrorDetails
from unyt import unyt_quantity

from flow360.component.simulation.conversion import (
need_conversion,
require,
unit_converter,
)
from flow360.component.simulation.conversion import need_conversion, unit_converter
from flow360.component.simulation.unit_system import LengthType
from flow360.component.simulation.validation import validation_context
from flow360.component.types import COMMENTS, TYPE_TAG_STR
from flow360.error_messages import do_not_modify_file_manually_msg
from flow360.exceptions import Flow360FileError
from flow360.log import log
Expand Down Expand Up @@ -552,14 +546,48 @@ def _calculate_hash(cls, model_dict):
hasher.update(json_string.encode("utf-8"))
return hasher.hexdigest()

def _convert_unit(
self,
*,
exclude: List[str] = None,
to_unit_system: Literal["flow360_v2", "SI", "Imperial", "CGS"] = "flow360_v2",
) -> dict:
solver_values = {}
self_dict = self.__dict__

if exclude is None:
exclude = []

additional_fields = {}

for property_name, value in chain(self_dict.items(), additional_fields.items()):
if need_conversion(value) and property_name not in exclude:
log.debug(f" -> need conversion for: {property_name} = {value}")
# pylint: disable=no-member
solver_values[property_name] = value.in_base(unit_system=to_unit_system)
log.debug(f" converted to: {solver_values[property_name]}")
elif isinstance(value, list) and property_name not in exclude:
new_value = []
for item in value:
if need_conversion(item):
# pylint: disable=no-member
new_value.append(item.in_base(unit_system=to_unit_system))
else:
new_value.append(item)
solver_values[property_name] = new_value
else:
solver_values[property_name] = value

return solver_values

# pylint: disable=too-many-arguments, too-many-locals, too-many-branches
def _convert_dimensions_to_solver(
def _nondimensionalization(
self,
*,
params,
mesh_unit: LengthType.Positive = None,
exclude: List[str] = None,
required_by: List[str] = None,
extra: List[Any] = None,
) -> dict:
solver_values = {}
self_dict = self.__dict__
Expand All @@ -571,17 +599,9 @@ def _convert_dimensions_to_solver(
required_by = []

additional_fields = {}
if extra is not None:
for extra_item in extra:
# Note: we should not be expecting extra field for SimulationParam?
require(extra_item.dependency_list, required_by, params)
additional_fields[extra_item.name] = extra_item.value_factory()

assert mesh_unit is not None

for property_name, value in chain(self_dict.items(), additional_fields.items()):
if property_name in [COMMENTS, TYPE_TAG_STR]:
continue
loc_name = property_name
field = self.model_fields.get(property_name)
if field is not None and field.alias is not None:
Expand Down Expand Up @@ -637,6 +657,9 @@ def preprocess(
params : SimulationParams
Full config definition as Flow360Params.
mesh_unit: LengthType.Positive
The lenght represented by 1 unit length in the mesh.
exclude: List[str] (optional)
List of fields to not convert to solver dimensions.
Expand All @@ -655,9 +678,14 @@ def preprocess(
if required_by is None:
required_by = []

solver_values = self._convert_dimensions_to_solver(params, mesh_unit, exclude, required_by)
solver_values = self._nondimensionalization(
params=params,
mesh_unit=mesh_unit,
exclude=exclude,
required_by=required_by,
)
for property_name, value in self.__dict__.items():
if property_name in [COMMENTS, TYPE_TAG_STR] + exclude:
if property_name in exclude:
continue
loc_name = property_name
field = self.model_fields.get(property_name)
Expand All @@ -679,7 +707,55 @@ def preprocess(
required_by=[*required_by, loc_name, f"{i}"],
exclude=exclude,
)
elif isinstance(item, unyt_quantity):
solver_values[property_name][i] = item.in_base(unit_system="flow360_v2")

return self.__class__(**solver_values)

def convert_to_unit_system(
self,
*,
to_unit_system: Literal["SI", "Imperial", "CGS"],
exclude: List[str] = None,
) -> Flow360BaseModel:
"""
Loops through all fields and performs unit conversion to given unit system.
Separated from preprocess() to allow unit conversion only. preprocess may contain additonal processing.
Parameters
----------
params : SimulationParams
Full config definition as Flow360Params.
exclude: List[str] (optional)
List of fields to not convert to solver dimensions.
Returns
-------
caller class
returns caller class with units all in flow360 base unit system
"""

if exclude is None:
exclude = []

solver_values = self._convert_unit(
exclude=exclude,
to_unit_system=to_unit_system,
)
for property_name, value in self.__dict__.items():
if property_name in exclude:
continue
if isinstance(value, Flow360BaseModel):
solver_values[property_name] = value.convert_to_unit_system(
exclude=exclude,
to_unit_system=to_unit_system,
)
elif isinstance(value, list):
for i, item in enumerate(value):
if isinstance(item, Flow360BaseModel):
solver_values[property_name][i] = item.convert_to_unit_system(
exclude=exclude,
to_unit_system=to_unit_system,
)

return self.__class__(**solver_values)
30 changes: 20 additions & 10 deletions flow360/component/simulation/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,21 +208,21 @@ def get_default_params(
)


def _intersect_validation_levels(requested_levels, avaliable_levels):
if requested_levels is not None:
def _intersect_validation_levels(requested_levels, available_levels):
if requested_levels is not None and available_levels is not None:
if requested_levels == ALL:
validation_levels_to_use = [
item for item in ["SurfaceMesh", "VolumeMesh", "Case"] if item in avaliable_levels
item for item in ["SurfaceMesh", "VolumeMesh", "Case"] if item in available_levels
]
elif isinstance(requested_levels, str):
if requested_levels in avaliable_levels:
if requested_levels in available_levels:
validation_levels_to_use = [requested_levels]
else:
validation_levels_to_use = None
else:
assert isinstance(requested_levels, list)
validation_levels_to_use = [
item for item in requested_levels if item in avaliable_levels
item for item in requested_levels if item in available_levels
]
return validation_levels_to_use
return None
Expand All @@ -235,7 +235,7 @@ def validate_model(
validation_level: Union[
Literal["SurfaceMesh", "VolumeMesh", "Case", "All"], list, None
] = ALL, # Fix implicit string concatenation
) -> Tuple[Optional[dict], Optional[list], Optional[list]]:
) -> Tuple[Optional[SimulationParams], Optional[list], Optional[list]]:
"""
Validate a params dict against the pydantic model.
Expand All @@ -250,7 +250,7 @@ def validate_model(
Returns
-------
validated_param : dict or None
validated_param : SimulationParams or None
The validated parameters if successful, otherwise None.
validation_errors : list or None
A list of validation errors if any occurred.
Expand All @@ -268,10 +268,10 @@ def validate_model(

params_as_dict = clean_params_dict(params_as_dict, root_item_type)

# The final validaiton levels will be the intersection of the requested levels and the levels available
# The final validation levels will be the intersection of the requested levels and the levels available
# We always assume we want to run case so that we can expose as many errors as possible
avaliable_levels = _determine_validation_level(up_to="Case", root_item_type=root_item_type)
validation_levels_to_use = _intersect_validation_levels(validation_level, avaliable_levels)
available_levels = _determine_validation_level(up_to="Case", root_item_type=root_item_type)
validation_levels_to_use = _intersect_validation_levels(validation_level, available_levels)
try:
params_as_dict = parse_model_dict(params_as_dict, globals())
with unit_system:
Expand Down Expand Up @@ -579,3 +579,13 @@ def generate_process_json(
case_res = _process_case(params, mesh_unit, up_to)

return surface_mesh_res, volume_mesh_res, case_res


def change_unit_system(
*, simulation_params: SimulationParams, target_unit_system: Literal["SI", "Imperial", "CGS"]
):
"""
Changes the unit system of the simulation parameters and convert the values accordingly.
"""
converted_params = simulation_params.convert_to_unit_system(unit_system=target_unit_system)
return converted_params.model_dump_json(exclude_none=True)
32 changes: 31 additions & 1 deletion flow360/component/simulation/simulation_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from __future__ import annotations

from typing import Annotated, List, Optional, Union
from typing import Annotated, List, Literal, Optional, Union

import pydantic as pd

Expand Down Expand Up @@ -57,6 +57,7 @@
from flow360.component.simulation.user_defined_dynamics.user_defined_dynamics import (
UserDefinedDynamic,
)
from flow360.component.simulation.utils import model_attribute_unlock
from flow360.component.simulation.validation.validation_output import (
_check_output_fields,
)
Expand Down Expand Up @@ -244,6 +245,35 @@ def preprocess(self, mesh_unit=None, exclude: list = None) -> SimulationParams:
return super().preprocess(params=self, mesh_unit=mesh_unit, exclude=exclude)
return super().preprocess(params=self, mesh_unit=mesh_unit, exclude=exclude)

def convert_to_unit_system(
self,
unit_system: Literal["SI", "Imperial", "CGS"],
exclude: list = None,
) -> SimulationParams:
"""Internal function for non-dimensionalizing the simulation parameters"""
if exclude is None:
exclude = []

if unit_system not in ["SI", "Imperial", "CGS"]:
raise Flow360ConfigurationError(
f"Invalid unit system: {unit_system}. Must be one of ['SI', 'Imperial', 'CGS']"
)
converted_param = None
if unit_system_manager.current is None:
# pylint: disable=not-context-manager
with self.unit_system:
converted_param = super().convert_to_unit_system(
to_unit_system=unit_system, exclude=exclude
)
else:
converted_param = super().convert_to_unit_system(
to_unit_system=unit_system, exclude=exclude
)
# Change the recorded unit system
with model_attribute_unlock(converted_param, "unit_system"):
converted_param.unit_system = UnitSystem.from_dict(**{"name": unit_system})
return converted_param

# pylint: disable=no-self-argument
@pd.field_validator("models", mode="after")
@classmethod
Expand Down
Loading

0 comments on commit f6cdae6

Please sign in to comment.