From 00eb6973cffc1e5bb1a943e031f6915a691e9718 Mon Sep 17 00:00:00 2001 From: Yifan Bai Date: Tue, 18 Jun 2024 23:20:40 +0000 Subject: [PATCH 1/2] Add translator for rotation Update rotation model name Fix unit test Address comments Make cylinders in test fixture fix unit test Fix unit tests Added TODO item Added type and name for models and refinements Fix unit test --- flow360/component/simulation/exposed_units.py | 2 +- .../simulation/meshing_param/edge_params.py | 1 + .../simulation/meshing_param/face_params.py | 2 + .../simulation/meshing_param/volume_params.py | 2 + .../simulation/models/surface_models.py | 42 +- .../simulation/models/volume_models.py | 31 +- .../component/simulation/simulation_params.py | 8 +- .../translator/solver_translator.py | 33 +- flow360/component/simulation/unit_system.py | 1 + .../framework/test_unit_system_v2.py | 29 +- .../params/test_simulation_params.py | 11 +- .../service/test_translator_service.py | 1 + ...Flow360_xv15_bet_disk_nested_rotation.json | 527 ++++++++++++++++++ .../translator/test_solver_translator.py | 14 + ...15BETDiskNestedRotation_param_generator.py | 83 +++ 15 files changed, 733 insertions(+), 54 deletions(-) create mode 100644 tests/simulation/translator/ref/Flow360_xv15_bet_disk_nested_rotation.json create mode 100644 tests/simulation/translator/utils/xv15BETDiskNestedRotation_param_generator.py diff --git a/flow360/component/simulation/exposed_units.py b/flow360/component/simulation/exposed_units.py index cf7a20567..463cefa67 100644 --- a/flow360/component/simulation/exposed_units.py +++ b/flow360/component/simulation/exposed_units.py @@ -19,7 +19,7 @@ "pressure": [], "density": [], "viscosity": [], - "angular_velocity": [], + "angular_velocity": [u.rpm], "heat_flux": [], "heat_source": [], "specific_heat_capacity": [], diff --git a/flow360/component/simulation/meshing_param/edge_params.py b/flow360/component/simulation/meshing_param/edge_params.py index b7245b910..03f78c489 100644 --- a/flow360/component/simulation/meshing_param/edge_params.py +++ b/flow360/component/simulation/meshing_param/edge_params.py @@ -53,6 +53,7 @@ class SurfaceEdgeRefinement(_BaseEdgeRefinement): (equivalent to `ProjectAniso` in old params). """ + name: Optional[str] = pd.Field(None) refinement_type: Literal["SurfaceEdgeRefinement"] = pd.Field( "SurfaceEdgeRefinement", frozen=True ) diff --git a/flow360/component/simulation/meshing_param/face_params.py b/flow360/component/simulation/meshing_param/face_params.py index dd343eda7..df49eeaa4 100644 --- a/flow360/component/simulation/meshing_param/face_params.py +++ b/flow360/component/simulation/meshing_param/face_params.py @@ -23,6 +23,7 @@ class SurfaceRefinement(Flow360BaseModel): these defaults so that the when face name is not present, what config we ues. Depending on how we go down the road. """ + name: Optional[str] = pd.Field(None) refinement_type: Literal["SurfaceRefinement"] = pd.Field("SurfaceRefinement", frozen=True) entities: Optional[EntityList[Surface]] = pd.Field(None, alias="faces") # pylint: disable=no-member @@ -51,6 +52,7 @@ class BoundaryLayer(Flow360BaseModel): need to have dedicated field for global settings. This is also consistent with the `FluidDynamics` class' design. """ + name: Optional[str] = pd.Field(None) refinement_type: Literal["BoundaryLayer"] = pd.Field("BoundaryLayer", frozen=True) type: Literal["aniso", "projectAnisoSpacing", "none"] = pd.Field() entities: Optional[EntityList[Surface]] = pd.Field(None, alias="faces") diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index 6503757b8..83c5bbf27 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -34,6 +34,7 @@ class AxisymmetricRefinement(Flow360BaseModel): the mesh data. But this currently is out of scope due to the estimated efforts. """ + name: Optional[str] = pd.Field(None) refinement_type: Literal["AxisymmetricRefinement"] = pd.Field( "AxisymmetricRefinement", frozen=True ) @@ -52,6 +53,7 @@ class RotationCylinder(AxisymmetricRefinement): 78d442233fa944e6af8eed4de9541bb1?pvs=4#c2de0b822b844a12aa2c00349d1f68a3 """ + name: Optional[str] = pd.Field(None) enclosed_objects: Optional[EntityList[Cylinder, Surface]] = pd.Field( None, description="""Entities enclosed by this sliding interface. Can be faces, boxes and/or other cylinders etc. diff --git a/flow360/component/simulation/models/surface_models.py b/flow360/component/simulation/models/surface_models.py index a5482129a..0651b502e 100644 --- a/flow360/component/simulation/models/surface_models.py +++ b/flow360/component/simulation/models/surface_models.py @@ -83,6 +83,26 @@ class Mach(SingleAttributeModel): value: pd.NonNegativeFloat = pd.Field() +class Translational(Flow360BaseModel): + """Translational periodicity""" + + type: Literal["Translational"] = pd.Field("Translational", frozen=True) + + +class Rotational(Flow360BaseModel): + """Rotational periodicity""" + + type: Literal["Rotational"] = pd.Field("Rotational", frozen=True) + # pylint: disable=fixme + # TODO: Maybe we need more precision when serializeing this one? + axis_of_rotation: Optional[Axis] = pd.Field(None) + + +########################################## +############# Surface models ############# +########################################## + + class Wall(BoundaryBase): """Replace Flow360Param: - NoSlipWall @@ -93,6 +113,7 @@ class Wall(BoundaryBase): - SolidAdiabaticWall """ + name: Optional[str] = pd.Field(None) type: Literal["Wall"] = pd.Field("Wall", frozen=True) use_wall_function: bool = pd.Field(False) velocity: Optional[VelocityVectorType] = pd.Field(None) @@ -103,6 +124,7 @@ class Wall(BoundaryBase): class Freestream(BoundaryBaseWithTurbulenceQuantities): """Freestream""" + name: Optional[str] = pd.Field(None) type: Literal["Freestream"] = pd.Field("Freestream", frozen=True) velocity: Optional[VelocityVectorType] = pd.Field(None) velocity_type: Literal["absolute", "relative"] = pd.Field("relative") @@ -115,6 +137,7 @@ class Outflow(BoundaryBase): - MassOutflow """ + name: Optional[str] = pd.Field(None) type: Literal["Outflow"] = pd.Field("Outflow", frozen=True) spec: Union[Pressure, MassFlowRate, Mach] = pd.Field() @@ -125,6 +148,7 @@ class Inflow(BoundaryBaseWithTurbulenceQuantities): - MassInflow """ + name: Optional[str] = pd.Field(None) type: Literal["Inflow"] = pd.Field("Inflow", frozen=True) # pylint: disable=no-member total_temperature: TemperatureType.Positive = pd.Field() @@ -135,36 +159,24 @@ class Inflow(BoundaryBaseWithTurbulenceQuantities): class SlipWall(BoundaryBase): """Slip wall""" + name: Optional[str] = pd.Field(None) type: Literal["SlipWall"] = pd.Field("SlipWall", frozen=True) class SymmetryPlane(BoundaryBase): """Symmetry plane""" + name: Optional[str] = pd.Field(None) type: Literal["SymmetryPlane"] = pd.Field("SymmetryPlane", frozen=True) -class Translational(Flow360BaseModel): - """Translational""" - - type: Literal["Translational"] = pd.Field("Translational", frozen=True) - - -class Rotational(Flow360BaseModel): - """Rotational""" - - type: Literal["Rotational"] = pd.Field("Rotational", frozen=True) - # pylint: disable=fixme - # TODO: Maybe we need more precision when serializeing this one? - axis_of_rotation: Optional[Axis] = pd.Field(None) - - class Periodic(Flow360BaseModel): """Replace Flow360Param: - TranslationallyPeriodic - RotationallyPeriodic """ + name: Optional[str] = pd.Field(None) type: Literal["Periodic"] = pd.Field("Periodic", frozen=True) entity_pairs: UniqueItemList[SurfacePair] = pd.Field(alias="surface_pairs") spec: Union[Translational, Rotational] = pd.Field(discriminator="type") diff --git a/flow360/component/simulation/models/volume_models.py b/flow360/component/simulation/models/volume_models.py index b72e5fc76..5f73fee43 100644 --- a/flow360/component/simulation/models/volume_models.py +++ b/flow360/component/simulation/models/volume_models.py @@ -6,9 +6,7 @@ from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.framework.entity_base import EntityList -from flow360.component.simulation.framework.single_attribute_base import ( - SingleAttributeModel, -) +from flow360.component.simulation.framework.expressions import StringExpression from flow360.component.simulation.models.material import ( Air, FluidMaterialTypes, @@ -37,9 +35,10 @@ from flow360.component.types import Axis -# pylint: disable=missing-class-docstring -class AngularVelocity(SingleAttributeModel): - value: AngularVelocityType = pd.Field() +class FromUserDefinedDynamics(Flow360BaseModel): + """Rotation is controlled by user defined dynamics""" + + type: Literal["FromUserDefinedDynamics"] = pd.Field("FromUserDefinedDynamics", frozen=True) class ExpressionInitialConditionBase(Flow360BaseModel): @@ -80,6 +79,7 @@ class Fluid(PDEModelBase): General FluidDynamics volume model that contains all the common fields every fluid dynamics zone should have. """ + type: Literal["Fluid"] = pd.Field("Fluid", frozen=True) navier_stokes_solver: NavierStokesSolver = pd.Field(NavierStokesSolver()) turbulence_model_solver: TurbulenceModelSolverType = pd.Field(SpalartAllmaras()) transition_model_solver: Optional[TransitionModelSolver] = pd.Field(None) @@ -99,6 +99,8 @@ class Solid(PDEModelBase): General HeatTransfer volume model that contains all the common fields every heat transfer zone should have. """ + name: Optional[str] = pd.Field(None) + type: Literal["Solid"] = pd.Field("Solid", frozen=True) entities: EntityList[GenericVolume, str] = pd.Field(alias="volumes") material: SolidMaterialTypes = pd.Field() @@ -168,6 +170,8 @@ class ActuatorDisk(Flow360BaseModel): Note that `center`, `axis_thrust`, `thickness` can be acquired from `entity` so they are not required anymore. """ + name: Optional[str] = pd.Field(None) + type: Literal["ActuatorDisk"] = pd.Field("ActuatorDisk", frozen=True) entities: Optional[EntityList[Cylinder]] = pd.Field(None, alias="volumes") force_per_area: ForcePerArea = pd.Field() @@ -203,6 +207,8 @@ class BETDisk(Flow360BaseModel): so they are not required anymore. """ + name: Optional[str] = pd.Field(None) + type: Literal["BETDisk"] = pd.Field("BETDisk", frozen=True) entities: Optional[EntityList[Cylinder]] = pd.Field(None, alias="volumes") rotation_direction_rule: Literal["leftHand", "rightHand"] = pd.Field("rightHand") @@ -222,21 +228,26 @@ class BETDisk(Flow360BaseModel): sectional_radiuses: List[float] = pd.Field() -class RotatingReferenceFrame(Flow360BaseModel): +class Rotation(Flow360BaseModel): """Similar to Flow360Param ReferenceFrame. Note that `center`, `axis` can be acquired from `entity` so they are not required anymore. Note: Should use the unit system to convert degree or degree per second to radian and radian per second """ + name: Optional[str] = pd.Field(None) + type: Literal["Rotation"] = pd.Field("Rotation", frozen=True) entities: EntityList[GenericVolume, Cylinder, str] = pd.Field(alias="volumes") - rotation: Union[AngularVelocity, AngleType] = pd.Field() - parent_volume_name: Optional[Union[GenericVolume, str]] = pd.Field(None) + # TODO: Add test for each of the spec specification. + spec: Union[StringExpression, FromUserDefinedDynamics, AngularVelocityType] = pd.Field() + parent_volume: Optional[Union[GenericVolume, Cylinder, str]] = pd.Field(None) class PorousMedium(Flow360BaseModel): """Constains Flow360Param PorousMediumBox and PorousMediumVolumeZone""" + name: Optional[str] = pd.Field(None) + type: Literal["PorousMedium"] = pd.Field("PorousMedium", frozen=True) entities: Optional[EntityList[GenericVolume, Box, str]] = pd.Field(None, alias="volumes") darcy_coefficient: InverseAreaType.Point = pd.Field() @@ -251,6 +262,6 @@ class PorousMedium(Flow360BaseModel): Solid, ActuatorDisk, BETDisk, - RotatingReferenceFrame, + Rotation, PorousMedium, ] diff --git a/flow360/component/simulation/simulation_params.py b/flow360/component/simulation/simulation_params.py index f4c53e59e..f8edc0629 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import List, Optional, Union +from typing import Annotated, List, Optional, Union import pydantic as pd @@ -33,6 +33,10 @@ from flow360.exceptions import Flow360ConfigurationError, Flow360RuntimeError from flow360.version import __version__ +AllowedModelTypes = Annotated[ + Union[VolumeModelTypes, SurfaceModelTypes], pd.Field(discriminator="type") +] + class AssetCache(Flow360BaseModel): """ @@ -210,7 +214,7 @@ class SimulationParams(_ParamModelBase): 3. by_name(pattern:str) to use regexpr/glob to select all zones/surfaces with matched name 3. by_type(pattern:str) to use regexpr/glob to select all zones/surfaces with matched type """ - models: Optional[List[Union[VolumeModelTypes, SurfaceModelTypes]]] = pd.Field(None) + models: Optional[List[AllowedModelTypes]] = pd.Field(None) """ Below can be mostly reused with existing models """ diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index b733a3b51..a7a42fdb1 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -9,7 +9,7 @@ SymmetryPlane, Wall, ) -from flow360.component.simulation.models.volume_models import BETDisk, Fluid +from flow360.component.simulation.models.volume_models import BETDisk, Fluid, Rotation from flow360.component.simulation.outputs.outputs import ( SliceOutput, SurfaceOutput, @@ -211,6 +211,9 @@ def get_solver_json( replace_dict_key(disk_param, "machNumbers", "MachNumbers") replace_dict_key(disk_param, "reynoldsNumbers", "ReynoldsNumbers") volumes = disk_param.pop("volumes") + for extra_attr in ["name", "type"]: + if extra_attr in disk_param: + disk_param.pop(extra_attr) for v in volumes["storedEntities"]: disk_i = deepcopy(disk_param) disk_i["axisOfRotation"] = v["axis"] @@ -221,11 +224,33 @@ def get_solver_json( bet_disks.append(disk_i) translated["BETDisks"] = bet_disks - ##:: Step 8: Get porous media + ##:: Step 8: Get rotation + for model in input_params.models: + if isinstance(model, Rotation): + for volume in model.entities.stored_entities: + volumeZone = { + "modelType": "FluidDynamics", + "referenceFrame": { + "axisOfRotation": list(volume.axis), + "centerOfRotation": list(volume.center), + }, + } + if model.parent_volume: + volumeZone["referenceFrame"]["parentVolumeName"] = model.parent_volume.name + spec = dump_dict(model)["spec"] + if isinstance(spec, str): + volumeZone["referenceFrame"]["thetaRadians"] = spec + elif spec.get("units", "") == "flow360_angular_velocity_unit": + volumeZone["referenceFrame"]["omegaRadians"] = spec["value"] + volumeZones = translated.get("volumeZones", {}) + volumeZones.update({volume.name: volumeZone}) + translated["volumeZones"] = volumeZones + + ##:: Step 9: Get porous media - ##:: Step 9: Get heat transfer zones + ##:: Step 10: Get heat transfer zones - ##:: Step 10: Get user defined dynamics + ##:: Step 11: Get user defined dynamics if input_params.user_defined_dynamics is not None: translated["userDefinedDynamics"] = [] for udd in input_params.user_defined_dynamics: diff --git a/flow360/component/simulation/unit_system.py b/flow360/component/simulation/unit_system.py index 327ba4261..436a550b1 100644 --- a/flow360/component/simulation/unit_system.py +++ b/flow360/component/simulation/unit_system.py @@ -708,6 +708,7 @@ class _AngularVelocityType(_DimensionedType): dim = udim.angular_velocity dim_name = "angular_velocity" + has_defaults = False AngularVelocityType = Annotated[_AngularVelocityType, PlainSerializer(_dimensioned_type_serializer)] diff --git a/tests/simulation/framework/test_unit_system_v2.py b/tests/simulation/framework/test_unit_system_v2.py index bd79af88d..b7ebe07af 100644 --- a/tests/simulation/framework/test_unit_system_v2.py +++ b/tests/simulation/framework/test_unit_system_v2.py @@ -185,7 +185,7 @@ def test_flow360_unit_arithmetic(): pt=(1, 1, 1), vec=(1, 1, 1), ax=(1, 1, 1), - omega=(1, 1, 1), + omega=(1, 1, 1) * u.flow360_angular_velocity_unit, ) assert data == data_flow360 @@ -248,14 +248,13 @@ def test_unit_system(): "p": 5, "r": 2, "mu": 3, - "omega": 5, "m_dot": 11, "v_sq": 123, "fqc": 1111, } # SI with u.SI_unit_system: - data = DataWithUnits(**input, a=1 * u.degree) + data = DataWithUnits(**input, a=1 * u.degree, omega=1 * u.radian / u.s) assert data.L == 1 * u.m assert data.m == 2 * u.kg @@ -267,14 +266,13 @@ def test_unit_system(): assert data.p == 5 * u.Pa assert data.r == 2 * u.kg / u.m**3 assert data.mu == 3 * u.Pa * u.s - assert data.omega == 5 * u.rad / u.s assert data.m_dot == 11 * u.kg / u.s assert data.v_sq == 123 * u.m**2 / u.s**2 assert data.fqc == 1111 / u.s # CGS with u.CGS_unit_system: - data = DataWithUnits(**input, a=1 * u.degree) + data = DataWithUnits(**input, a=1 * u.degree, omega=1 * u.radian / u.s) assert data.L == 1 * u.cm assert data.m == 2 * u.g @@ -286,14 +284,13 @@ def test_unit_system(): assert data.p == 5 * u.dyne / u.cm**2 assert data.r == 2 * u.g / u.cm**3 assert data.mu == 3 * u.dyn * u.s / u.cm**2 - assert data.omega == 5 * u.rad / u.s assert data.m_dot == 11 * u.g / u.s assert data.v_sq == 123 * u.cm**2 / u.s**2 assert data.fqc == 1111 / u.s # Imperial with u.imperial_unit_system: - data = DataWithUnits(**input, a=1 * u.degree) + data = DataWithUnits(**input, a=1 * u.degree, omega=1 * u.radian / u.s) assert data.L == 1 * u.ft assert data.m == 2 * u.lb @@ -305,14 +302,15 @@ def test_unit_system(): assert data.p == 5 * u.lbf / u.ft**2 assert data.r == 2 * u.lb / u.ft**3 assert data.mu == 3 * u.lbf * u.s / u.ft**2 - assert data.omega == 5 * u.rad / u.s assert data.m_dot == 11 * u.lb / u.s assert data.v_sq == 123 * u.ft**2 / u.s**2 assert data.fqc == 1111 / u.s # Flow360 with u.flow360_unit_system: - data = DataWithUnits(**input, a=1 * u.flow360_angle_unit) + data = DataWithUnits( + **input, a=1 * u.flow360_angle_unit, omega=1 * u.flow360_angular_velocity_unit + ) assert data.L == 1 * u.flow360_length_unit assert data.m == 2 * u.flow360_mass_unit @@ -324,7 +322,6 @@ def test_unit_system(): assert data.p == 5 * u.flow360_pressure_unit assert data.r == 2 * u.flow360_density_unit assert data.mu == 3 * u.flow360_viscosity_unit - assert data.omega == 5 * u.flow360_angular_velocity_unit assert data.m_dot == 11 * u.flow360_mass_flow_rate_unit assert data.v_sq == 123 * u.flow360_specific_energy_unit assert data.fqc == 1111 * u.flow360_frequency_unit @@ -341,7 +338,7 @@ def test_unit_system(): "p": 5, "r": 2, "mu": 3, - "omega": 5, + "omega": 1 * u.radian / u.s, "m_dot": 10, "v_sq": 0.2, "fqc": 123, @@ -473,19 +470,21 @@ def test_unit_system(): with u.SI_unit_system: # Note that for union types the first element of union that passes validation is inferred! - data = VectorDataWithUnits(pt=(1, 1, 1), vec=(1, 1, 1), ax=(1, 1, 1), omega=(1, 1, 1)) + data = VectorDataWithUnits( + pt=(1, 1, 1), vec=(1, 1, 1), ax=(1, 1, 1), omega=(1, 1, 1) * u.rpm + ) assert all(coord == 1 * u.m for coord in data.pt) assert all(coord == 1 * u.m / u.s for coord in data.vec) assert all(coord == 1 * u.m for coord in data.ax) - assert all(coord == 1 * u.rad / u.s for coord in data.omega) + assert all(coord == 1 * u.rpm for coord in data.omega) - data = VectorDataWithUnits(pt=None, vec=(1, 1, 1), ax=(1, 1, 1), omega=(1, 1, 1)) + data = VectorDataWithUnits(pt=None, vec=(1, 1, 1), ax=(1, 1, 1), omega=(1, 1, 1) * u.rpm) assert data.pt is None assert all(coord == 1 * u.m / u.s for coord in data.vec) assert all(coord == 1 * u.m for coord in data.ax) - assert all(coord == 1 * u.rad / u.s for coord in data.omega) + assert all(coord == 1 * u.rpm for coord in data.omega) def test_optionals_and_unions(): diff --git a/tests/simulation/params/test_simulation_params.py b/tests/simulation/params/test_simulation_params.py index 071c932ed..f278c83ac 100644 --- a/tests/simulation/params/test_simulation_params.py +++ b/tests/simulation/params/test_simulation_params.py @@ -18,10 +18,9 @@ TurbulenceQuantities, ) from flow360.component.simulation.models.volume_models import ( - AngularVelocity, Fluid, PorousMedium, - RotatingReferenceFrame, + Rotation, Solid, ) from flow360.component.simulation.operating_condition import ( @@ -102,9 +101,7 @@ def get_the_param(): heat_spec=HeatFlux(1.0 * u.W / u.m**2), ), SlipWall(entities=[my_slip_wall_surface]), - RotatingReferenceFrame( - volumes=[my_cylinder_1], rotation=AngularVelocity(0.45 * u.rad / u.s) - ), + Rotation(volumes=[my_cylinder_1], spec=0.45 * u.rad / u.s), PorousMedium( volumes=[my_box], darcy_coefficient=(0.1, 2, 1.0) / u.cm / u.m, @@ -149,7 +146,7 @@ def get_the_param(): @pytest.mark.usefixtures("array_equality_override") -def test_simulation_params_seralization(get_the_param): +def test_simulation_params_serialization(get_the_param): to_file_from_file_test(get_the_param) @@ -181,7 +178,7 @@ def test_simulation_params_unit_conversion(get_the_param): 1.0005830903790088e-11, ) # AngularVelocityType - assertions.assertAlmostEqual(converted.models[3].rotation.value.value, 0.01296006) + assertions.assertAlmostEqual(converted.models[3].spec.value, 0.01296006) # HeatFluxType assertions.assertAlmostEqual(converted.models[1].heat_spec.value.value, 2.47809322e-11) # HeatSourceType diff --git a/tests/simulation/service/test_translator_service.py b/tests/simulation/service/test_translator_service.py index 2effee662..848fdec0f 100644 --- a/tests/simulation/service/test_translator_service.py +++ b/tests/simulation/service/test_translator_service.py @@ -179,6 +179,7 @@ def test_simulation_to_case_json(): param_data = { "models": [ { + "type": "Fluid", "material": { "dynamic_viscosity": { "effective_temperature": {"units": "K", "value": 111.0}, diff --git a/tests/simulation/translator/ref/Flow360_xv15_bet_disk_nested_rotation.json b/tests/simulation/translator/ref/Flow360_xv15_bet_disk_nested_rotation.json new file mode 100644 index 000000000..6d2557dea --- /dev/null +++ b/tests/simulation/translator/ref/Flow360_xv15_bet_disk_nested_rotation.json @@ -0,0 +1,527 @@ +{ + "BETDisks": [ + { + "MachNumbers": [ + 0.0 + ], + "ReynoldsNumbers": [ + 1000000.0 + ], + "alphas": [ + -16.0, + -14.0, + -12.0, + -10.0, + -8.0, + -6.0, + -4.0, + -2.0, + 0.0, + 2.0, + 4.0, + 6.0, + 8.0, + 10.0, + 12.0, + 14.0, + 16.0 + ], + "axisOfRotation": [ + 0.0, + 0.0, + 1.0 + ], + "bladeLineChord": 25.0, + "centerOfRotation": [ + 0.0, + 0.0, + 0.0 + ], + "chordRef": 14.0, + "chords": [ + { + "chord": 0.0, + "radius": 13.4999999 + }, + { + "chord": 17.69622361, + "radius": 13.5 + }, + { + "chord": 14.012241185039136, + "radius": 37.356855462705056 + }, + { + "chord": 14.004512929656503, + "radius": 150.0348189415042 + } + ], + "initialBladeDirection": [ + 1.0, + 0.0, + 0.0 + ], + "nLoadingNodes": 20, + "numberOfBlades": 3, + "omega": 0.002299999956366251, + "radius": 150.0, + "rotationDirectionRule": "leftHand", + "sectionalPolars": [ + { + "dragCoeffs": [ + [ + [ + 0.04476, + 0.0372, + 0.02956, + 0.02272, + 0.01672, + 0.01157, + 0.00757, + 0.00762, + 0.00789, + 0.00964, + 0.01204, + 0.01482, + 0.01939, + 0.02371, + 0.02997, + 0.03745, + 0.05013 + ] + ] + ], + "liftCoeffs": [ + [ + [ + -0.4805, + -0.3638, + -0.2632, + -0.162, + -0.0728, + -0.0045, + 0.0436, + 0.2806, + 0.4874, + 0.6249, + 0.7785, + 0.9335, + 1.0538, + 1.1929, + 1.302, + 1.4001, + 1.4277 + ] + ] + ] + }, + { + "dragCoeffs": [ + [ + [ + 0.03864, + 0.03001, + 0.0226, + 0.01652, + 0.01212, + 0.00933, + 0.00623, + 0.00609, + 0.00622, + 0.00632, + 0.00666, + 0.00693, + 0.00747, + 0.00907, + 0.01526, + 0.02843, + 0.04766 + ] + ] + ], + "liftCoeffs": [ + [ + [ + -0.875, + -0.7769, + -0.6771, + -0.5777, + -0.4689, + -0.3714, + -0.1894, + 0.0577, + 0.3056, + 0.552, + 0.7908, + 1.0251, + 1.2254, + 1.3668, + 1.4083, + 1.3681, + 1.307 + ] + ] + ] + }, + { + "dragCoeffs": [ + [ + [ + 0.03864, + 0.03001, + 0.0226, + 0.01652, + 0.01212, + 0.00933, + 0.00623, + 0.00609, + 0.00622, + 0.00632, + 0.00666, + 0.00693, + 0.00747, + 0.00907, + 0.01526, + 0.02843, + 0.04766 + ] + ] + ], + "liftCoeffs": [ + [ + [ + -0.875, + -0.7769, + -0.6771, + -0.5777, + -0.4689, + -0.3714, + -0.1894, + 0.0577, + 0.3056, + 0.552, + 0.7908, + 1.0251, + 1.2254, + 1.3668, + 1.4083, + 1.3681, + 1.307 + ] + ] + ] + }, + { + "dragCoeffs": [ + [ + [ + 0.02636, + 0.01909, + 0.01426, + 0.01174, + 0.00999, + 0.00864, + 0.00679, + 0.00484, + 0.00469, + 0.00479, + 0.00521, + 0.00797, + 0.01019, + 0.01213, + 0.0158, + 0.02173, + 0.03066 + ] + ] + ], + "liftCoeffs": [ + [ + [ + -1.3633, + -1.2672, + -1.1603, + -1.0317, + -0.8378, + -0.6231, + -0.4056, + -0.1813, + 0.0589, + 0.2997, + 0.5369, + 0.7455, + 0.9432, + 1.1111, + 1.2157, + 1.3208, + 1.4081 + ] + ] + ] + }, + { + "dragCoeffs": [ + [ + [ + 0.02328, + 0.01785, + 0.01414, + 0.01116, + 0.00925, + 0.00782, + 0.00685, + 0.00585, + 0.00402, + 0.00417, + 0.00633, + 0.00791, + 0.00934, + 0.01102, + 0.01371, + 0.01744, + 0.02419 + ] + ] + ], + "liftCoeffs": [ + [ + [ + -1.4773, + -1.3857, + -1.2144, + -1.0252, + -0.8141, + -0.5945, + -0.3689, + -0.1411, + 0.0843, + 0.3169, + 0.5378, + 0.7576, + 0.973, + 1.1754, + 1.3623, + 1.4883, + 1.5701 + ] + ] + ] + }, + { + "dragCoeffs": [ + [ + [ + 0.02328, + 0.01785, + 0.01414, + 0.01116, + 0.00925, + 0.03246, + 0.00696, + 0.00565, + 0.00408, + 0.00399, + 0.0061, + 0.00737, + 0.00923, + 0.01188, + 0.01665, + 0.02778, + 0.04379 + ] + ] + ], + "liftCoeffs": [ + [ + [ + -1.4773, + -1.3857, + -1.2144, + -1.0252, + -0.8141, + -0.5158, + -0.3378, + -0.1137, + 0.11, + 0.3331, + 0.5499, + 0.77, + 0.9846, + 1.1893, + 1.3707, + 1.4427, + 1.4346 + ] + ] + ] + } + ], + "sectionalRadiuses": [ + 13.5, + 25.5, + 37.5, + 76.5, + 120.0, + 150.0 + ], + "thickness": 15.0, + "tipGap": "inf", + "twists": [ + { + "radius": 13.5, + "twist": 40.29936539609504 + }, + { + "radius": 25.5, + "twist": 36.047382700278234 + }, + { + "radius": 37.356855, + "twist": 31.01189770991256 + }, + { + "radius": 76.5, + "twist": 16.596477306554306 + }, + { + "radius": 120.0, + "twist": 8.488574045325713 + }, + { + "radius": 150.0, + "twist": 3.97516 + } + ] + } + ], + "boundaries": { + "1": { + "type": "Freestream", + "velocityType": "relative" + } + }, + "freestream": { + "Mach": 0, + "MachRef": 0.69, + "Temperature": 288.15, + "alphaAngle": -90.0, + "betaAngle": 0.0, + "muRef": 1.95151e-06 + }, + "geometry": { + "momentCenter": [ + 0.0, + 0.0, + 0.0 + ], + "momentLength": [ + 1.0, + 1.0, + 1.0 + ], + "refArea": 70685.83470577035 + }, + "navierStokesSolver": { + "CFLMultiplier": 1.0, + "absoluteTolerance": 1e-10, + "equationEvalFrequency": 1, + "kappaMUSCL": -1.0, + "limitPressureDensity": false, + "limitVelocity": false, + "linearSolver": { + "maxIterations": 25 + }, + "lowMachPreconditioner": false, + "maxForceJacUpdatePhysicalSteps": 0, + "modelType": "Compressible", + "numericalDissipationFactor": 1.0, + "orderOfAccuracy": 2, + "relativeTolerance": 0.0, + "updateJacobianFrequency": 4 + }, + "surfaceOutput": { + "animationFrequency": -1, + "animationFrequencyOffset": 0, + "animationFrequencyTimeAverage": -1, + "animationFrequencyTimeAverageOffset": 0, + "computeTimeAverages": false, + "outputFields": [], + "outputFormat": "paraview,tecplot", + "startAverageIntegrationStep": -1, + "surfaces": { + "1": { + "outputFields": [ + "primitiveVars", + "Cp", + "Cf", + "CfVec" + ] + } + }, + "writeSingleFile": false + }, + "timeStepping": { + "CFL": { + "final": 10000.0, + "initial": 100.0, + "rampSteps": 15, + "type": "ramp" + }, + "maxPseudoSteps": 30, + "orderOfAccuracy": 2, + "physicalSteps": 1800, + "timeStepSize": 7.588388196110053 + }, + "turbulenceModelSolver": { + "modelType": "None" + }, + "volumeOutput": { + "animationFrequency": -1, + "animationFrequencyOffset": 0, + "animationFrequencyTimeAverage": -1, + "animationFrequencyTimeAverageOffset": 0, + "computeTimeAverages": false, + "outputFields": [ + "primitiveVars", + "betMetrics", + "qcriterion" + ], + "outputFormat": "tecplot", + "startAverageIntegrationStep": -1 + }, + "volumeZones": { + "inner": { + "modelType": "FluidDynamics", + "referenceFrame": { + "axisOfRotation": [ + 0.0, + 0.0, + 1.0 + ], + "centerOfRotation": [ + 0.0, + 0.0, + 0.0 + ], + "omegaRadians": -0.0009999999810288047, + "parentVolumeName": "middle" + } + }, + "middle": { + "modelType": "FluidDynamics", + "referenceFrame": { + "axisOfRotation": [ + 0.0, + 0.0, + 1.0 + ], + "centerOfRotation": [ + 0.0, + 0.0, + 0.0 + ], + "thetaRadians": "-0.001299999975337446*t;" + } + } + } +} diff --git a/tests/simulation/translator/test_solver_translator.py b/tests/simulation/translator/test_solver_translator.py index 57c9356b0..261325405 100644 --- a/tests/simulation/translator/test_solver_translator.py +++ b/tests/simulation/translator/test_solver_translator.py @@ -33,6 +33,11 @@ create_unsteady_hover_param, create_unsteady_hover_UDD_param, ) +from tests.simulation.translator.utils.xv15BETDiskNestedRotation_param_generator import ( + create_nested_rotation_param, + cylinder_inner, + cylinder_middle, +) from tests.utils import compare_values @@ -149,3 +154,12 @@ def test_xv15_bet_disk( translate_and_compare( param, mesh_unit=1 * u.inch, ref_json_file="Flow360_xv15_bet_disk_unsteady_hover_UDD.json" ) + + +def test_xv15_bet_disk_nested_rotation( + create_nested_rotation_param, cylinder_inner, cylinder_middle +): + param = create_nested_rotation_param + translate_and_compare( + param, mesh_unit=1 * u.inch, ref_json_file="Flow360_xv15_bet_disk_nested_rotation.json" + ) diff --git a/tests/simulation/translator/utils/xv15BETDiskNestedRotation_param_generator.py b/tests/simulation/translator/utils/xv15BETDiskNestedRotation_param_generator.py new file mode 100644 index 000000000..b040bf566 --- /dev/null +++ b/tests/simulation/translator/utils/xv15BETDiskNestedRotation_param_generator.py @@ -0,0 +1,83 @@ +import pytest +from numpy import pi + +import flow360.component.simulation.units as u +from flow360.component.simulation.models.volume_models import Rotation +from flow360.component.simulation.primitives import Cylinder +from tests.simulation.translator.utils.xv15_bet_disk_helper import ( + createBETDiskUnsteady, + createUnsteadyTimeStepping, +) +from tests.simulation.translator.utils.xv15BETDisk_param_generator import ( + _BET_cylinder, + create_param_base, +) + + +@pytest.fixture +def cylinder_inner(): + return Cylinder( + name="inner", + center=(0, 0, 0) * u.inch, + axis=[0, 0, 1], + # filler values + outer_radius=50 * u.inch, + height=15 * u.inch, + ) + + +@pytest.fixture +def cylinder_middle(): + return Cylinder( + name="middle", + center=(0, 0, 0) * u.inch, + axis=[0, 0, 1], + inner_radius=50 * u.inch, + outer_radius=100 * u.inch, + height=15 * u.inch, + ) + + +@pytest.fixture +def create_nested_rotation_param(cylinder_inner, cylinder_middle): + """ + params = runCase_unsteady_hover( + pitch_in_degree=10, + rpm_bet=294.25225, + rpm_inner=-127.93576086956521, + rpm_middle=-166.31648913043477, + CTRef=0.008073477299631027, + CQRef=0.0007044185338787385, + tolerance=1.25e-2, + caseName="runCase_unsteady_hover-2" + ) + """ + rpm_bet = 294.25225 + rpm_inner = -127.93576086956521 + rpm_middle = -166.31648913043477 + mesh_unit = 1 * u.inch + params = create_param_base() + bet_disk = createBETDiskUnsteady(_BET_cylinder, 10, rpm_bet) + rotation_inner = Rotation( + volumes=[cylinder_inner], + spec=rpm_inner * u.rpm, + parent_volume=cylinder_middle, + ) + omega_middle = ( + rpm_middle + / 60 + * 2 + * pi + * u.radian + / u.s + * mesh_unit + / params.operating_condition.thermal_state.speed_of_sound + ) + rotation_middle = Rotation( + volumes=[cylinder_middle], + spec=str(omega_middle.v.item()) + "*t", + ) + params.models += [bet_disk, rotation_inner, rotation_middle] + params.time_stepping = createUnsteadyTimeStepping(rpm_bet - rpm_inner - rpm_middle) + params.time_stepping.max_pseudo_steps = 30 + return params From f16a3fec5b03b0807567b0cf85217e6416fbadd1 Mon Sep 17 00:00:00 2001 From: Yifan Bai Date: Fri, 21 Jun 2024 21:06:54 +0000 Subject: [PATCH 2/2] Use util function for model expansion --- .../simulation/models/volume_models.py | 2 +- .../translator/solver_translator.py | 66 +++++++++++-------- .../component/simulation/translator/utils.py | 21 +++++- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/flow360/component/simulation/models/volume_models.py b/flow360/component/simulation/models/volume_models.py index 5f73fee43..4ead6186d 100644 --- a/flow360/component/simulation/models/volume_models.py +++ b/flow360/component/simulation/models/volume_models.py @@ -22,7 +22,6 @@ ) from flow360.component.simulation.primitives import Box, Cylinder, GenericVolume from flow360.component.simulation.unit_system import ( - AngleType, AngularVelocityType, HeatSourceType, InverseAreaType, @@ -48,6 +47,7 @@ class ExpressionInitialConditionBase(Flow360BaseModel): constants: Optional[Dict[str, str]] = pd.Field() +# pylint: disable=missing-class-docstring class NavierStokesInitialCondition(ExpressionInitialConditionBase): rho: str = pd.Field() u: str = pd.Field() diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index a7a42fdb1..7049b2de5 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -26,6 +26,7 @@ remove_units_in_dict, replace_dict_key, replace_dict_value, + translate_setting_and_apply_to_all_entities, ) from flow360.component.simulation.unit_system import LengthType @@ -35,13 +36,6 @@ def dump_dict(input_params): return input_params.model_dump(by_alias=True, exclude_none=True) -def remove_empty_keys(input_dict): - """I do not know what this is for --- Ben""" - # pylint: disable=fixme - # TODO: implement - return input_dict - - def init_output_attr_dict(obj_list, class_type): """Initialize the common output attribute.""" return { @@ -53,6 +47,32 @@ def init_output_attr_dict(obj_list, class_type): } +def rotation_entity_info_serializer(volume): + """Rotation entity serializer""" + return { + "referenceFrame": { + "axisOfRotation": list(volume.axis), + "centerOfRotation": list(volume.center), + }, + } + + +def rotation_translator(model: Rotation): + """Rotation translator""" + volume_zone = { + "modelType": "FluidDynamics", + "referenceFrame": {}, + } + if model.parent_volume: + volume_zone["referenceFrame"]["parentVolumeName"] = model.parent_volume.name + spec = dump_dict(model)["spec"] + if isinstance(spec, str): + volume_zone["referenceFrame"]["thetaRadians"] = spec + elif spec.get("units", "") == "flow360_angular_velocity_unit": + volume_zone["referenceFrame"]["omegaRadians"] = spec["value"] + return volume_zone + + # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals @@ -225,26 +245,18 @@ def get_solver_json( translated["BETDisks"] = bet_disks ##:: Step 8: Get rotation - for model in input_params.models: - if isinstance(model, Rotation): - for volume in model.entities.stored_entities: - volumeZone = { - "modelType": "FluidDynamics", - "referenceFrame": { - "axisOfRotation": list(volume.axis), - "centerOfRotation": list(volume.center), - }, - } - if model.parent_volume: - volumeZone["referenceFrame"]["parentVolumeName"] = model.parent_volume.name - spec = dump_dict(model)["spec"] - if isinstance(spec, str): - volumeZone["referenceFrame"]["thetaRadians"] = spec - elif spec.get("units", "") == "flow360_angular_velocity_unit": - volumeZone["referenceFrame"]["omegaRadians"] = spec["value"] - volumeZones = translated.get("volumeZones", {}) - volumeZones.update({volume.name: volumeZone}) - translated["volumeZones"] = volumeZones + if has_instance_in_list(input_params.models, Rotation): + volume_zones = translated.get("volumeZones", {}) + volume_zones.update( + translate_setting_and_apply_to_all_entities( + input_params.models, + Rotation, + rotation_translator, + to_list=False, + entity_injection_func=rotation_entity_info_serializer, + ) + ) + translated["volumeZones"] = volume_zones ##:: Step 9: Get porous media diff --git a/flow360/component/simulation/translator/utils.py b/flow360/component/simulation/translator/utils.py index 75effdb37..4852aa9da 100644 --- a/flow360/component/simulation/translator/utils.py +++ b/flow360/component/simulation/translator/utils.py @@ -139,12 +139,26 @@ def get_attribute_from_first_instance( return None +def update_dict_recursively(a, b): + """ + Recursively updates dictionary 'a' with values from dictionary 'b'. + If the same key contains dictionaries in both 'a' and 'b', they are merged recursively. + """ + for key, value in b.items(): + if key in a and isinstance(a[key], dict) and isinstance(value, dict): + # If both a[key] and b[key] are dictionaries, recurse + update_dict_recursively(a[key], value) + else: + # Otherwise, simply update/overwrite the value in 'a' with the value from 'b' + a[key] = value + + def translate_setting_and_apply_to_all_entities( obj_list: list, class_type, translation_func, to_list: bool = False, - entity_injection_func=lambda x: x, + entity_injection_func=lambda x: {}, ): """Translate settings and apply them to all entities of a given type. @@ -170,8 +184,9 @@ def translate_setting_and_apply_to_all_entities( for entity in obj.entities.stored_entities: if not to_list: if output.get(entity.name) is None: - output[entity.name] = {} - output[entity.name].update(translated_setting) + output[entity.name] = entity_injection_func(entity) + # needs to be recursive + update_dict_recursively(output[entity.name], translated_setting) else: setting = entity_injection_func(entity) setting.update(translated_setting)