diff --git a/flow360/component/simulation/operating_condition/operating_condition.py b/flow360/component/simulation/operating_condition/operating_condition.py index d56454740..f2cb17e9c 100644 --- a/flow360/component/simulation/operating_condition/operating_condition.py +++ b/flow360/component/simulation/operating_condition/operating_condition.py @@ -208,7 +208,8 @@ class GenericReferenceCondition(MultiConstructorBaseModel): velocity_magnitude: Optional[VelocityType.Positive] = ConditionalField( context=CASE, description="Freestream velocity magnitude. Used as reference velocity magnitude" - + " when :py:attr:`reference_velocity_magnitude` is not specified.", + + " when :py:attr:`reference_velocity_magnitude` is not specified. Cannot change once specified.", + frozen=True, ) thermal_state: ThermalState = pd.Field( ThermalState(), @@ -233,6 +234,12 @@ def mach(self) -> pd.PositiveFloat: """Computes Mach number.""" return self.velocity_magnitude / self.thermal_state.speed_of_sound + @pd.field_validator("thermal_state", mode="after") + @classmethod + def _update_input_cache(cls, value, info: pd.ValidationInfo): + setattr(info.data["private_attribute_input_cache"], info.field_name, value) + return value + class AerospaceConditionCache(Flow360BaseModel): """[INTERNAL] Cache for AerospaceCondition inputs""" @@ -275,6 +282,7 @@ class AerospaceCondition(MultiConstructorBaseModel): description="Freestream velocity magnitude. Used as reference velocity magnitude" + " when :py:attr:`reference_velocity_magnitude` is not specified.", context=CASE, + frozen=True, ) thermal_state: ThermalState = pd.Field( ThermalState(), @@ -284,6 +292,7 @@ class AerospaceCondition(MultiConstructorBaseModel): reference_velocity_magnitude: Optional[VelocityType.Positive] = CaseField( None, description="Reference velocity magnitude. Is required when :py:attr:`velocity_magnitude` is 0.", + frozen=True, ) private_attribute_input_cache: AerospaceConditionCache = AerospaceConditionCache() @@ -381,6 +390,12 @@ def mach(self) -> pd.PositiveFloat: """Computes Mach number.""" return self.velocity_magnitude / self.thermal_state.speed_of_sound + @pd.field_validator("alpha", "beta", "thermal_state", mode="after") + @classmethod + def _update_input_cache(cls, value, info: pd.ValidationInfo): + setattr(info.data["private_attribute_input_cache"], info.field_name, value) + return value + # pylint: disable=fixme # TODO: AutomotiveCondition diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index 6a2209901..31701bed6 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -264,16 +264,24 @@ class Box(MultiConstructorBaseModel, _VolumeEntityBase): """ type_name: Literal["Box"] = pd.Field("Box", frozen=True) - private_attribute_entity_type_name: Literal["Box"] = pd.Field("Box", frozen=True) # pylint: disable=no-member center: LengthType.Point = pd.Field(description="The coordinates of the center of the box.") size: LengthType.PositiveVector = pd.Field( description="The dimensions of the box (length, width, height)." ) - axis_of_rotation: Axis = pd.Field(default=(0, 0, 1), description="The rotation axis.") - angle_of_rotation: AngleType = pd.Field(default=0 * u.degree, description="The rotation angle.") - private_attribute_input_cache: BoxCache = pd.Field(BoxCache(), frozen=True) + axis_of_rotation: Axis = pd.Field( + default=(0, 0, 1), + description="The rotation axis. Cannot change once specified.", + frozen=True, + ) + angle_of_rotation: AngleType = pd.Field( + default=0 * u.degree, + description="The rotation angle. Cannot change once specified.", + frozen=True, + ) private_attribute_id: str = pd.Field(default_factory=generate_uuid, frozen=True) + private_attribute_input_cache: BoxCache = pd.Field(BoxCache(), frozen=True) + private_attribute_entity_type_name: Literal["Box"] = pd.Field("Box", frozen=True) # pylint: disable=no-self-argument @MultiConstructorBaseModel.model_constructor @@ -344,6 +352,12 @@ def axes(self): """Return the axes that the box is aligned with.""" return self.private_attribute_input_cache.axes + @pd.field_validator("center", "size", mode="after") + @classmethod + def _update_input_cache(cls, value, info: pd.ValidationInfo): + setattr(info.data["private_attribute_input_cache"], info.field_name, value) + return value + @final class Cylinder(_VolumeEntityBase): diff --git a/tests/ref/simulation/service_init_geometry.json b/tests/ref/simulation/service_init_geometry.json index 650ef7df7..a26ea2ceb 100644 --- a/tests/ref/simulation/service_init_geometry.json +++ b/tests/ref/simulation/service_init_geometry.json @@ -48,7 +48,47 @@ "operating_condition": { "type_name": "AerospaceCondition", "private_attribute_constructor": "default", - "private_attribute_input_cache": {}, + "private_attribute_input_cache": { + "alpha": { + "value": 0.0, + "units": "degree" + }, + "beta": { + "value": 0.0, + "units": "degree" + }, + "thermal_state": { + "type_name": "ThermalState", + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "value": 288.15, + "units": "K" + }, + "density": { + "value": 1.225, + "units": "kg/m**3" + }, + "material": { + "type": "air", + "name": "air", + "dynamic_viscosity": { + "reference_viscosity": { + "value": 1.716e-05, + "units": "Pa*s" + }, + "reference_temperature": { + "value": 273.15, + "units": "K" + }, + "effective_temperature": { + "value": 110.4, + "units": "K" + } + } + } + } + }, "alpha": { "value": 0.0, "units": "degree" diff --git a/tests/ref/simulation/service_init_volume_mesh.json b/tests/ref/simulation/service_init_volume_mesh.json index 81929d8a0..6b6dda0a1 100644 --- a/tests/ref/simulation/service_init_volume_mesh.json +++ b/tests/ref/simulation/service_init_volume_mesh.json @@ -28,7 +28,47 @@ "operating_condition": { "type_name": "AerospaceCondition", "private_attribute_constructor": "default", - "private_attribute_input_cache": {}, + "private_attribute_input_cache": { + "alpha": { + "value": 0.0, + "units": "degree" + }, + "beta": { + "value": 0.0, + "units": "degree" + }, + "thermal_state": { + "type_name": "ThermalState", + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "value": 288.15, + "units": "K" + }, + "density": { + "value": 1.225, + "units": "kg/m**3" + }, + "material": { + "type": "air", + "name": "air", + "dynamic_viscosity": { + "reference_viscosity": { + "value": 1.716e-05, + "units": "Pa*s" + }, + "reference_temperature": { + "value": 273.15, + "units": "K" + }, + "effective_temperature": { + "value": 110.4, + "units": "K" + } + } + } + } + }, "alpha": { "value": 0.0, "units": "degree" diff --git a/tests/simulation/framework/test_multi_constructor_model.py b/tests/simulation/framework/test_multi_constructor_model.py index 371938868..c95a201b6 100644 --- a/tests/simulation/framework/test_multi_constructor_model.py +++ b/tests/simulation/framework/test_multi_constructor_model.py @@ -175,3 +175,31 @@ class ModelWithEntityList(Flow360BaseModel): data_parsed = parse_model_dict(incomplete_data, globals()) assert sorted(data_parsed.items()) == sorted(full_data.items()) + + +def test_entity_modification(get_aerospace_condition_using_from): + + my_box = Box.from_principal_axes( + name="box", + axes=[(0, 1, 0), (0, 0, 1)], + center=(0, 0, 0) * u.m, + size=(0.2, 0.3, 2) * u.m, + ) + + my_box.center = (1, 2, 3) * u.m + assert all(my_box.private_attribute_input_cache.center == (1, 2, 3) * u.m) + + my_box = Box( + name="box2", + axis_of_rotation=(1, 0, 0), + angle_of_rotation=45 * u.deg, + center=(1, 1, 1) * u.m, + size=(0.2, 0.3, 2) * u.m, + ) + + 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.alpha = -12 * u.rad + assert my_op.private_attribute_input_cache.alpha == -12 * u.rad