From 0bb6da808624bcc16d9b4954f0ff18197fe9e404 Mon Sep 17 00:00:00 2001 From: Wiktor Date: Wed, 28 May 2025 16:24:28 +0200 Subject: [PATCH] [SCFD-3899] Added support for mach reynolds input in Aerospace condition (#961) * added operating condition from mach reynolds to aerospacecondition * black and isort * made exclude_none default in model_dump * fixed test * fixed solver translator unittests * Revert "fixed solver translator unittests" This reverts commit 1140140060b9b1da0ad6f522a9781fc3093c79d3. * Revert "fixed test" This reverts commit 4137165f63816608f6c6a8a40703ff7175159603. * Revert "made exclude_none default in model_dump" This reverts commit 5ece895146d1b8453a25cd5bf9b25a4d58011901. * added removing none fields for multi constructor model * small function name change to be more clear * Revert "small function name change to be more clear" This reverts commit 38358f9d67360d1919e2a663e6ff5c593cb0202f. * Revert "added removing none fields for multi constructor model" This reverts commit bae5e3d7b888784f6983da8603e15869921ab02a. * fixed conflicts in unittest * black * removed space for consistency * changed density to be calculated from mach and not mach ref * changed reynolds number function to be consistent with solver * added empty line so that list builds correctly * added backslashes to make infty symboo --------- Co-authored-by: Maciej Skarysz Co-authored-by: Ben <106089368+benflexcompute@users.noreply.github.com> --- flow360/__init__.py | 2 - .../operating_condition.py | 119 ++++++++++++++++-- .../framework/test_multi_constructor_model.py | 48 +++++-- .../params/test_simulation_params.py | 11 +- .../utils/tutorial_2dcrm_param_generator.py | 8 +- 5 files changed, 159 insertions(+), 29 deletions(-) diff --git a/flow360/__init__.py b/flow360/__init__.py index 5353ae195..bd6de45d5 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -92,7 +92,6 @@ AerospaceCondition, GenericReferenceCondition, ThermalState, - operating_condition_from_mach_reynolds, ) from flow360.component.simulation.outputs.output_entities import ( Isosurface, @@ -239,7 +238,6 @@ "Mach", "MassFlowRate", "UserDefinedField", - "operating_condition_from_mach_reynolds", "VolumeMesh", "SurfaceMesh", "UserDefinedFarfield", diff --git a/flow360/component/simulation/operating_condition/operating_condition.py b/flow360/component/simulation/operating_condition/operating_condition.py index 00f6c79e8..822de0d1a 100644 --- a/flow360/component/simulation/operating_condition/operating_condition.py +++ b/flow360/component/simulation/operating_condition/operating_condition.py @@ -246,8 +246,11 @@ class AerospaceConditionCache(Flow360BaseModel): """[INTERNAL] Cache for AerospaceCondition inputs""" mach: Optional[pd.NonNegativeFloat] = None + reynolds: Optional[pd.PositiveFloat] = None + project_length_unit: Optional[LengthType.Positive] = None alpha: Optional[AngleType] = None beta: Optional[AngleType] = None + temperature: Optional[AbsoluteTemperatureType] = None thermal_state: Optional[ThermalState] = pd.Field(None, alias="atmosphere") reference_mach: Optional[pd.PositiveFloat] = None @@ -372,6 +375,100 @@ def from_mach( reference_velocity_magnitude=reference_velocity_magnitude, ) + # pylint: disable=too-many-arguments + @MultiConstructorBaseModel.model_constructor + @pd.validate_call + def from_mach_reynolds( + cls, + mach: pd.PositiveFloat, + reynolds: pd.PositiveFloat, + project_length_unit: LengthType.Positive, + alpha: Optional[AngleType] = 0 * u.deg, + beta: Optional[AngleType] = 0 * u.deg, + temperature: AbsoluteTemperatureType = 288.15 * u.K, + reference_mach: Optional[pd.PositiveFloat] = None, + ): + """ + Create an `AerospaceCondition` from Mach number and Reynolds number. + + This function computes the thermal state based on the given Mach number, + Reynolds number, and temperature, and returns an `AerospaceCondition` object + initialized with the computed thermal state and given aerodynamic angles. + + Parameters + ---------- + mach : NonNegativeFloat + Freestream Mach number (must be non-negative). + reynolds : PositiveFloat + Freestream Reynolds number defined with mesh unit (must be positive). + project_length_unit: LengthType.Positive + Project length unit. + alpha : AngleType, optional + Angle of attack. Default is 0 degrees. + beta : AngleType, optional + Sideslip angle. Default is 0 degrees. + temperature : AbsoluteTemperatureType, optional + Freestream static temperature (must be a positive temperature value). Default is 288.15 Kelvin. + reference_mach : PositiveFloat, optional + Reference Mach number. Default is None. + + Returns + ------- + AerospaceCondition + An instance of :class:`AerospaceCondition` with calculated velocity, thermal state and provided parameters. + + Example + ------- + Example usage: + + >>> condition = operating_condition_from_mach_reynolds( + ... mach=0.85, + ... reynolds=1e6, + ... project_length_unit=1 * u.mm, + ... temperature=288.15 * u.K, + ... alpha=2.0 * u.deg, + ... beta=0.0 * u.deg, + ... reference_mach=0.85, + ... ) + >>> print(condition) + AerospaceCondition(...) + + """ + + if temperature.units is u.K and temperature.value == 288.15: + log.info("Default value of 288.15 K will be used as temperature.") + + material = Air() + + velocity = mach * material.get_speed_of_sound(temperature) + + density = ( + reynolds + * material.get_dynamic_viscosity(temperature) + / (velocity * project_length_unit) + ) + + thermal_state = ThermalState(temperature=temperature, density=density) + + velocity_magnitude = mach * thermal_state.speed_of_sound + + reference_velocity_magnitude = ( + reference_mach * thermal_state.speed_of_sound if reference_mach else None + ) + + log.info( + """Density and viscosity were calculated based on input data, ThermalState will be automatically created.""" + ) + + # pylint: disable=no-value-for-parameter + return cls( + velocity_magnitude=velocity_magnitude, + alpha=alpha, + beta=beta, + thermal_state=thermal_state, + reference_velocity_magnitude=reference_velocity_magnitude, + ) + @pd.model_validator(mode="after") @context_validator(context=CASE) def check_valid_reference_velocity(self) -> Self: @@ -401,10 +498,10 @@ def _update_input_cache(cls, value, info: pd.ValidationInfo): def flow360_reynolds_number(self, length_unit: LengthType.Positive): """ Computes length_unit based Reynolds number. - :math:`Re = \\rho_{\\infty} \\cdot U_{ref} \\cdot L_{grid}/\\mu_{\\infty}` where - - :math:`rho_{\\infty}` is the freestream fluid density. - - :math:`U_{ref}` is the reference velocity magnitude or freestream velocity magnitude if reference - velocity magnitude is not set. + :math:`Re = \\rho_{\\infty} \\cdot U_{\\infty} \\cdot L_{grid}/\\mu_{\\infty}` where + + - :math:`\\rho_{\\infty}` is the freestream fluid density. + - :math:`U_{\\infty}` is the freestream velocity magnitude. - :math:`L_{grid}` is physical length represented by unit length in the given mesh/geometry file. - :math:`\\mu_{\\infty}` is the dynamic eddy viscosity of the fluid of freestream. @@ -413,14 +510,10 @@ def flow360_reynolds_number(self, length_unit: LengthType.Positive): length_unit : LengthType.Positive Physical length represented by unit length in the given mesh/geometry file. """ - reference_velocity = ( - self.reference_velocity_magnitude - if self.reference_velocity_magnitude - else self.velocity_magnitude - ) + return ( self.thermal_state.density - * reference_velocity + * self.velocity_magnitude * length_unit / self.thermal_state.dynamic_viscosity ).value @@ -428,6 +521,7 @@ def flow360_reynolds_number(self, length_unit: LengthType.Positive): # pylint: disable=fixme # TODO: AutomotiveCondition +<<<<<<< HEAD OperatingConditionTypes = Union[GenericReferenceCondition, AerospaceCondition] @@ -525,3 +619,8 @@ def operating_condition_from_mach_reynolds( thermal_state=thermal_state, reference_mach=reference_mach, ) +======= +OperatingConditionTypes = Union[ + GenericReferenceCondition, AerospaceCondition, LiquidOperatingCondition +] +>>>>>>> 962fc438 ([SCFD-3899] Added support for mach reynolds input in Aerospace condition (#961)) diff --git a/tests/simulation/framework/test_multi_constructor_model.py b/tests/simulation/framework/test_multi_constructor_model.py index f0e03afd4..2b5670119 100644 --- a/tests/simulation/framework/test_multi_constructor_model.py +++ b/tests/simulation/framework/test_multi_constructor_model.py @@ -42,7 +42,7 @@ def get_aerospace_condition_default_and_thermal_state_using_from(): @pytest.fixture -def get_aerospace_condition_using_from(): +def get_aerospace_condition_using_from_mach(): return AerospaceCondition.from_mach( mach=0.8, alpha=5 * u.deg, @@ -50,25 +50,45 @@ def get_aerospace_condition_using_from(): ) +@pytest.fixture +def get_aerospace_condition_using_from_mach_reynolds(): + return AerospaceCondition.from_mach_reynolds( + mach=0.8, + reynolds=1e6, + project_length_unit=u.m, + alpha=5 * u.deg, + temperature=290 * u.K, + ) + + def compare_objects_from_dict(dict1: dict, dict2: dict, object_class: type[Flow360BaseModel]): obj1 = object_class.model_validate(dict1) obj2 = object_class.model_validate(dict2) assert obj1.model_dump_json() == obj2.model_dump_json() -def test_full_model(get_aerospace_condition_default, get_aerospace_condition_using_from): +def test_full_model( + get_aerospace_condition_default, + get_aerospace_condition_using_from_mach, + get_aerospace_condition_using_from_mach_reynolds, +): full_data = get_aerospace_condition_default.model_dump(exclude_none=False) data_parsed = parse_model_dict(full_data, globals()) compare_objects_from_dict(full_data, data_parsed, AerospaceCondition) - full_data = get_aerospace_condition_using_from.model_dump(exclude_none=False) + full_data = get_aerospace_condition_using_from_mach.model_dump(exclude_none=False) + data_parsed = parse_model_dict(full_data, globals()) + compare_objects_from_dict(full_data, data_parsed, AerospaceCondition) + + full_data = get_aerospace_condition_using_from_mach_reynolds.model_dump(exclude_none=False) data_parsed = parse_model_dict(full_data, globals()) compare_objects_from_dict(full_data, data_parsed, AerospaceCondition) def test_incomplete_model( get_aerospace_condition_default, - get_aerospace_condition_using_from, + get_aerospace_condition_using_from_mach, + get_aerospace_condition_using_from_mach_reynolds, get_aerospace_condition_default_and_thermal_state_using_from, ): full_data = get_aerospace_condition_default.model_dump(exclude_none=False) @@ -77,7 +97,17 @@ def test_incomplete_model( data_parsed = parse_model_dict(incomplete_data, globals()) compare_objects_from_dict(full_data, data_parsed, AerospaceCondition) - full_data = get_aerospace_condition_using_from.model_dump(exclude_none=False) + full_data = get_aerospace_condition_using_from_mach.model_dump(exclude_none=False) + incomplete_data = { + "type_name": full_data["type_name"], + "private_attribute_constructor": full_data["private_attribute_constructor"], + "private_attribute_input_cache": full_data["private_attribute_input_cache"], + } + + data_parsed = parse_model_dict(incomplete_data, globals()) + compare_objects_from_dict(full_data, data_parsed, AerospaceCondition) + + full_data = get_aerospace_condition_using_from_mach_reynolds.model_dump(exclude_none=False) incomplete_data = { "type_name": full_data["type_name"], "private_attribute_constructor": full_data["private_attribute_constructor"], @@ -105,9 +135,9 @@ def test_incomplete_model( compare_objects_from_dict(full_data, data_parsed, AerospaceCondition) -def test_recursive_incomplete_model(get_aerospace_condition_using_from): +def test_recursive_incomplete_model(get_aerospace_condition_using_from_mach): # `incomplete_data` contains only the private_attribute_* for both the AerospaceCondition and ThermalState - full_data = get_aerospace_condition_using_from.model_dump(exclude_none=False) + full_data = get_aerospace_condition_using_from_mach.model_dump(exclude_none=False) input_cache = full_data["private_attribute_input_cache"] input_cache["thermal_state"] = { "type_name": input_cache["thermal_state"]["type_name"], @@ -184,7 +214,7 @@ class ModelWithEntityList(Flow360BaseModel): compare_objects_from_dict(full_data, data_parsed, ModelWithEntityList) -def test_entity_modification(get_aerospace_condition_using_from): +def test_entity_modification(get_aerospace_condition_using_from_mach): my_box = Box.from_principal_axes( name="box", @@ -207,7 +237,7 @@ def test_entity_modification(get_aerospace_condition_using_from): my_box.size = (1, 2, 32) * u.m assert all(my_box.private_attribute_input_cache.size == (1, 2, 32) * u.m) - my_op = get_aerospace_condition_using_from + my_op = get_aerospace_condition_using_from_mach my_op.alpha = -12 * u.rad assert my_op.private_attribute_input_cache.alpha == -12 * u.rad diff --git a/tests/simulation/params/test_simulation_params.py b/tests/simulation/params/test_simulation_params.py index f783d302d..44663eee1 100644 --- a/tests/simulation/params/test_simulation_params.py +++ b/tests/simulation/params/test_simulation_params.py @@ -40,7 +40,6 @@ from flow360.component.simulation.operating_condition.operating_condition import ( AerospaceCondition, ThermalState, - operating_condition_from_mach_reynolds, ) from flow360.component.simulation.primitives import ( Box, @@ -295,8 +294,12 @@ def test_subsequent_param_with_different_unit_system(): def test_mach_reynolds_op_cond(): +<<<<<<< HEAD condition = operating_condition_from_mach_reynolds( +======= + condition = AerospaceCondition.from_mach_reynolds( +>>>>>>> 962fc438 ([SCFD-3899] Added support for mach reynolds input in Aerospace condition (#961)) mach=0.2, reynolds=5e6, temperature=288.15 * u.K, @@ -307,7 +310,7 @@ def test_mach_reynolds_op_cond(): assertions.assertAlmostEqual(condition.thermal_state.dynamic_viscosity.value, 1.78929763e-5) assertions.assertAlmostEqual(condition.thermal_state.density.value, 1.31452332) - condition = operating_condition_from_mach_reynolds( + condition = AerospaceCondition.from_mach_reynolds( mach=0.2, reynolds=5e6, temperature=288.15 * u.K, @@ -316,10 +319,10 @@ def test_mach_reynolds_op_cond(): project_length_unit=u.m, reference_mach=0.4, ) - assertions.assertAlmostEqual(condition.thermal_state.density.value, 0.6572616596801923) + assertions.assertAlmostEqual(condition.thermal_state.density.value, 1.31452332) with pytest.raises(ValueError, match="Input should be greater than 0"): - condition = operating_condition_from_mach_reynolds( + condition = AerospaceCondition.from_mach_reynolds( mach=0.2, reynolds=0, temperature=288.15 * u.K, diff --git a/tests/simulation/translator/utils/tutorial_2dcrm_param_generator.py b/tests/simulation/translator/utils/tutorial_2dcrm_param_generator.py index 31d130687..508d81098 100644 --- a/tests/simulation/translator/utils/tutorial_2dcrm_param_generator.py +++ b/tests/simulation/translator/utils/tutorial_2dcrm_param_generator.py @@ -7,7 +7,7 @@ Wall, ) from flow360.component.simulation.operating_condition.operating_condition import ( - operating_condition_from_mach_reynolds, + AerospaceCondition, ) from flow360.component.simulation.primitives import ReferenceGeometry, Surface from flow360.component.simulation.simulation_params import SimulationParams @@ -24,7 +24,7 @@ def get_2dcrm_tutorial_param(): reference_geometry=ReferenceGeometry( moment_center=[0.25, 0.005, 0], moment_length=[1, 1, 1], area=0.01 ), - operating_condition=operating_condition_from_mach_reynolds( + operating_condition=AerospaceCondition.from_mach_reynolds( mach=0.2, reynolds=5e6, temperature=272.1 * u.K, @@ -52,7 +52,7 @@ def get_2dcrm_tutorial_param_deg_c(): reference_geometry=ReferenceGeometry( moment_center=[0.25, 0.005, 0], moment_length=[1, 1, 1], area=0.01 ), - operating_condition=operating_condition_from_mach_reynolds( + operating_condition=AerospaceCondition.from_mach_reynolds( mach=0.2, reynolds=5e6, temperature=-1.05 * u.degC, @@ -80,7 +80,7 @@ def get_2dcrm_tutorial_param_deg_f(): reference_geometry=ReferenceGeometry( moment_center=[0.25, 0.005, 0], moment_length=[1, 1, 1], area=0.01 ), - operating_condition=operating_condition_from_mach_reynolds( + operating_condition=AerospaceCondition.from_mach_reynolds( mach=0.2, reynolds=5e6, temperature=30.11 * u.degF,