Skip to content

[Hotfix 25.5]: [SCFD-3899] Added support for mach reynolds input in Aerospace condition #1103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions flow360/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@
GenericReferenceCondition,
LiquidOperatingCondition,
ThermalState,
operating_condition_from_mach_reynolds,
)
from flow360.component.simulation.outputs.output_entities import (
Isosurface,
Expand Down Expand Up @@ -254,7 +253,6 @@
"Mach",
"MassFlowRate",
"UserDefinedField",
"operating_condition_from_mach_reynolds",
"VolumeMesh",
"SurfaceMesh",
"UserDefinedFarfield",
Expand Down
209 changes: 103 additions & 106 deletions flow360/component/simulation/operating_condition/operating_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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,
)
48 changes: 39 additions & 9 deletions tests/simulation/framework/test_multi_constructor_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,33 +42,53 @@ 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,
thermal_state=ThermalState.from_standard_atmosphere(altitude=1000 * u.m),
)


@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)
Expand All @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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",
Expand All @@ -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

Expand Down
Loading