diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 4be39ac9d..b678a7822 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -49,6 +49,7 @@ from flow360.component.simulation.validation.validation_context import ( ALL, CASE, + TimeSteppingType, get_validation_info, get_validation_levels, ) @@ -192,6 +193,20 @@ class _AnimationSettings(_OutputBase): + " 0 is at beginning of simulation.", ) + @pd.field_validator("frequency", "frequency_offset") + @classmethod + def disable_frequecy_settings_in_steady_simulation(cls, value, info: pd.ValidationInfo): + """Disable frequency settings in a steady simulation""" + validation_info = get_validation_info() + if validation_info is None or validation_info.time_stepping != TimeSteppingType.STEADY: + return value + # pylint: disable=unsubscriptable-object + if value != cls.model_fields[info.field_name].default: + raise ValueError( + f"Output {info.field_name} cannot be specified in a steady simulation." + ) + return value + class _AnimationAndFileFormatSettings(_AnimationSettings): """ diff --git a/flow360/component/simulation/simulation_params.py b/flow360/component/simulation/simulation_params.py index b4d832823..e324f90c9 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -80,6 +80,7 @@ from flow360.component.simulation.utils import model_attribute_unlock from flow360.component.simulation.validation.validation_output import ( _check_output_fields, + _check_output_fields_valid_given_transition_model, _check_output_fields_valid_given_turbulence_model, _check_unsteadiness_to_use_aero_acoustics, ) @@ -511,6 +512,11 @@ def check_output_fields_valid_given_turbulence_model(params): """Check output fields are valid given the turbulence model""" return _check_output_fields_valid_given_turbulence_model(params) + @pd.model_validator(mode="after") + def check_output_fields_valid_given_transition_model(params): + """Check output fields are valid given the transition model""" + return _check_output_fields_valid_given_transition_model(params) + @pd.model_validator(mode="after") def check_and_add_rotating_reference_frame_model_flag_in_volumezones(params): """Ensure that all volume zones have the rotating_reference_frame_model flag with correct values""" diff --git a/flow360/component/simulation/validation/validation_output.py b/flow360/component/simulation/validation/validation_output.py index 340e6aa58..4bb1a8b2d 100644 --- a/flow360/component/simulation/validation/validation_output.py +++ b/flow360/component/simulation/validation/validation_output.py @@ -130,6 +130,39 @@ def _check_output_fields_valid_given_turbulence_model(params): return params +def _check_output_fields_valid_given_transition_model(params): + """Ensure that the output fields are consistent with the transition model used.""" + + if not params.models or not params.outputs: + return params + + transition_model = "None" + for model in params.models: + if isinstance(model, Fluid): + transition_model = model.transition_model_solver.type_name + break + + if transition_model != "None": + return params + + transition_output_fields = [ + "residualTransition", + "solutionTransition", + "linearResidualTransition", + ] + + for output_index, output in enumerate(params.outputs): + if output.output_type in ("AeroAcousticOutput", "StreamlineOutput"): + continue + for item in output.output_fields.items: + if isinstance(item, str) and item in transition_output_fields: + raise ValueError( + f"In `outputs`[{output_index}] {output.output_type}:, {item} is not a valid" + f" output field when transition model is not used." + ) + return params + + def _check_unsteadiness_to_use_aero_acoustics(params): if not params.outputs: diff --git a/tests/simulation/params/test_unit_conversions.py b/tests/simulation/params/test_unit_conversions.py index b24619b25..50973fb97 100644 --- a/tests/simulation/params/test_unit_conversions.py +++ b/tests/simulation/params/test_unit_conversions.py @@ -169,7 +169,7 @@ def test_operations_on_units(): assert str(replaced.units) == "dimensionless" replaced = params.operating_condition.velocity_magnitude**5 - (1 / 50 * (fl.u.km / fl.u.s) ** 5) - assertions.assertAlmostEqual(replaced.value, 502472105493.3395) + assertions.assertAlmostEqual(replaced.value, 502472105493.3395, 3) assert str(replaced.units) == "inch**5*m**5/(cm**5*s**5)" replaced = ( diff --git a/tests/simulation/params/test_validators_output.py b/tests/simulation/params/test_validators_output.py index cc117e2dd..d7f147c50 100644 --- a/tests/simulation/params/test_validators_output.py +++ b/tests/simulation/params/test_validators_output.py @@ -1,14 +1,19 @@ +import json +import os import re +import pydantic as pd import pytest import flow360 as fl import flow360.component.simulation.units as u +from flow360.component.simulation.framework.param_utils import AssetCache from flow360.component.simulation.models.solver_numerics import ( KOmegaSST, NoneSolver, SpalartAllmaras, ) +from flow360.component.simulation.models.surface_models import Wall from flow360.component.simulation.models.volume_models import Fluid from flow360.component.simulation.outputs.output_entities import Point from flow360.component.simulation.outputs.outputs import ( @@ -17,17 +22,23 @@ IsosurfaceOutput, ProbeOutput, SurfaceOutput, + SurfaceProbeOutput, TimeAverageSurfaceOutput, VolumeOutput, ) from flow360.component.simulation.primitives import Surface -from flow360.component.simulation.services import clear_context +from flow360.component.simulation.services import ( + ValidationCalledBy, + clear_context, + validate_model, +) from flow360.component.simulation.simulation_params import SimulationParams -from flow360.component.simulation.time_stepping.time_stepping import Unsteady +from flow360.component.simulation.time_stepping.time_stepping import Steady, Unsteady from flow360.component.simulation.unit_system import imperial_unit_system from flow360.component.simulation.user_code.core.types import UserVariable from flow360.component.simulation.user_code.functions import math from flow360.component.simulation.user_code.variables import solution +from flow360.component.volume_mesh import VolumeMeshV2 @pytest.fixture() @@ -129,6 +140,57 @@ def test_turbulence_enabled_output_fields(): ) +def test_transition_model_enabled_output_fields(): + with pytest.raises( + ValueError, + match=re.escape( + "In `outputs`[0] IsosurfaceOutput:, solutionTransition is not a valid output field when transition model is not used." + ), + ): + with imperial_unit_system: + SimulationParams( + models=[Fluid(transition_model_solver=NoneSolver())], + outputs=[ + IsosurfaceOutput( + name="iso", + entities=[Isosurface(name="tmp", field="mut", iso_value=1)], + output_fields=["solutionTransition"], + ) + ], + ) + + with pytest.raises( + ValueError, + match=re.escape( + "In `outputs`[0] SurfaceProbeOutput:, residualTransition is not a valid output field when transition model is not used." + ), + ): + with imperial_unit_system: + SimulationParams( + models=[Fluid(transition_model_solver=NoneSolver())], + outputs=[ + SurfaceProbeOutput( + name="probe_output", + probe_points=[Point(name="point_1", location=[1, 2, 3] * u.m)], + output_fields=["residualTransition"], + target_surfaces=[Surface(name="fluid/body")], + ) + ], + ) + + with pytest.raises( + ValueError, + match=re.escape( + "In `outputs`[0] VolumeOutput:, linearResidualTransition is not a valid output field when transition model is not used." + ), + ): + with imperial_unit_system: + SimulationParams( + models=[Fluid(transition_model_solver=NoneSolver())], + outputs=[VolumeOutput(output_fields=["linearResidualTransition"])], + ) + + def test_surface_user_variables_in_output_fields(): uv_surface1 = UserVariable( name="uv_surface1", value=math.dot(solution.velocity, solution.CfVec) @@ -227,3 +289,69 @@ def test_duplicate_surface_usage(): ], time_stepping=Unsteady(steps=10, step_size=1e-3), ) + + +def test_output_frequecy_settings_in_steady_simulation(): + + volume_mesh = VolumeMeshV2.from_local_storage( + mesh_id=None, + local_storage_path=os.path.join( + os.path.dirname(__file__), "..", "data", "vm_entity_provider" + ), + ) + with open( + os.path.join( + os.path.dirname(__file__), "..", "data", "vm_entity_provider", "simulation.json" + ), + "r", + ) as fh: + asset_cache_data = json.load(fh).pop("private_attribute_asset_cache") + asset_cache = AssetCache.model_validate(asset_cache_data) + with imperial_unit_system: + params = SimulationParams( + models=[Wall(name="wall", entities=volume_mesh["*"])], + time_stepping=Steady(), + outputs=[ + VolumeOutput( + output_fields=["Mach", "Cp"], + frequency=2, + ), + SurfaceOutput( + output_fields=["Cp"], + entities=volume_mesh["*"], + frequency_offset=10, + ), + ], + private_attribute_asset_cache=asset_cache, + ) + + params_as_dict = params.model_dump(exclude_none=True, mode="json") + print("haha", params_as_dict) + + params, errors, _ = validate_model( + params_as_dict=params_as_dict, + validated_by=ValidationCalledBy.LOCAL, + root_item_type="VolumeMesh", + validation_level="All", + ) + + expected_errors = [ + { + "loc": ("outputs", 0, "frequency"), + "type": "value_error", + "msg": "Value error, Output frequency cannot be specified in a steady simulation.", + "ctx": {"relevant_for": ["Case"]}, + }, + { + "loc": ("outputs", 1, "frequency_offset"), + "type": "value_error", + "msg": "Value error, Output frequency_offset cannot be specified in a steady simulation.", + "ctx": {"relevant_for": ["Case"]}, + }, + ] + assert len(errors) == len(expected_errors) + for err, exp_err in zip(errors, expected_errors): + assert err["loc"] == exp_err["loc"] + assert err["type"] == exp_err["type"] + assert err["ctx"]["relevant_for"] == exp_err["ctx"]["relevant_for"] + assert err["msg"] == exp_err["msg"] diff --git a/tests/simulation/params/test_validators_params.py b/tests/simulation/params/test_validators_params.py index e81b139ea..b1d45bd25 100644 --- a/tests/simulation/params/test_validators_params.py +++ b/tests/simulation/params/test_validators_params.py @@ -1647,7 +1647,6 @@ def test_validate_liquid_operating_condition(): ], outputs=[ VolumeOutput( - frequency=1, output_format="both", output_fields=["four"], ), @@ -1863,7 +1862,6 @@ def test_redefined_user_defined_fields(): ), outputs=[ VolumeOutput( - frequency=1, output_format="both", output_fields=["pressure"], ),