diff --git a/flow360/__init__.py b/flow360/__init__.py index dea86b3e3..ec8372d48 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -99,7 +99,6 @@ GenericReferenceCondition, LiquidOperatingCondition, ThermalState, - operating_condition_from_mach_reynolds, ) from flow360.component.simulation.outputs.output_entities import ( Isosurface, @@ -254,7 +253,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 0ae4a80ad..47ed17ab0 100644 --- a/flow360/component/simulation/operating_condition/operating_condition.py +++ b/flow360/component/simulation/operating_condition/operating_condition.py @@ -244,8 +244,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 @@ -370,6 +373,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: @@ -399,10 +496,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. @@ -411,14 +508,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 @@ -471,99 +564,3 @@ def check_valid_reference_velocity(self) -> Self: OperatingConditionTypes = Union[ GenericReferenceCondition, AerospaceCondition, LiquidOperatingCondition ] - - -# pylint: disable=too-many-arguments -@pd.validate_call -def operating_condition_from_mach_reynolds( - mach: pd.NonNegativeFloat, - reynolds: pd.PositiveFloat, - project_length_unit: LengthType.Positive = pd.Field( - description="The Length unit of the project." - ), - temperature: AbsoluteTemperatureType = 288.15 * u.K, - alpha: Optional[AngleType] = 0 * u.deg, - beta: Optional[AngleType] = 0 * u.deg, - reference_mach: Optional[pd.PositiveFloat] = None, -) -> AerospaceCondition: - """ - 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. - temperature : AbsoluteTemperatureType, optional - Freestream static temperature (must be a positive temperature value). Default is 288.15 Kelvin. - alpha : AngleType, optional - Angle of attack. Default is 0 degrees. - beta : AngleType, optional - Sideslip angle. Default is 0 degrees. - reference_mach : PositiveFloat, optional - Reference Mach number. Default is None. - - Returns - ------- - AerospaceCondition - An `AerospaceCondition` object initialized with the given parameters. - - Raises - ------ - ValidationError - If the input values do not meet the specified constraints. - ValueError - If required parameters are missing or calculations cannot be performed. - - 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 if reference_mach is None else reference_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) - - log.info( - """Density and viscosity were calculated based on input data, ThermalState will be automatically created.""" - ) - - # pylint: disable=no-value-for-parameter - return AerospaceCondition.from_mach( - mach=mach, - alpha=alpha, - beta=beta, - thermal_state=thermal_state, - reference_mach=reference_mach, - ) 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 a25b88118..b2905971a 100644 --- a/tests/simulation/params/test_simulation_params.py +++ b/tests/simulation/params/test_simulation_params.py @@ -46,7 +46,6 @@ AerospaceCondition, LiquidOperatingCondition, ThermalState, - operating_condition_from_mach_reynolds, ) from flow360.component.simulation.primitives import ( Box, @@ -345,7 +344,7 @@ def test_subsequent_param_with_different_unit_system(): def test_mach_reynolds_op_cond(): - condition = operating_condition_from_mach_reynolds( + condition = AerospaceCondition.from_mach_reynolds( mach=0.2, reynolds=5e6, temperature=288.15 * u.K, @@ -356,7 +355,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, @@ -365,10 +364,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,