From 78681a009b1f178f7a6da84d19f783637927fa17 Mon Sep 17 00:00:00 2001 From: BenYuan Date: Tue, 8 Oct 2024 14:11:37 +0000 Subject: [PATCH 1/9] Added BC completeness check --- flow360/component/simulation/entity_info.py | 39 +++++--- .../simulation/framework/base_model.py | 1 + .../simulation/framework/param_utils.py | 9 ++ .../operating_condition.py | 2 +- .../component/simulation/simulation_params.py | 40 +++++---- .../translator/surface_meshing_translator.py | 5 +- .../validation/validation_context.py | 4 +- .../validation_simulation_params.py | 50 ++++++++++- .../params/test_validators_params.py | 88 ++++++++++++++++++- 9 files changed, 203 insertions(+), 35 deletions(-) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index d2af664f3..b0a95e5ff 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -119,20 +119,27 @@ def _get_list_of_entities( entity_attribute_names = self.edge_attribute_names entity_full_list = self.grouped_edges + # Use the supplied one if not None if attribute_name is not None: - # pylint: disable=unsupported-membership-test,unsubscriptable-object - if attribute_name in entity_attribute_names: - # pylint: disable=no-member - return entity_full_list[entity_attribute_names.index(attribute_name)] - raise ValueError( - f"The given attribute_name {attribute_name} is not found" - f" in geometry metadata. Available: {entity_attribute_names}" + specified_attribute_name = attribute_name + else: + specified_attribute_name = ( + self.face_group_tag if entity_type_name == "face" else self.edge_group_tag ) - raise ValueError("Attribute name is required to get the full list of grouped entities.") + + # pylint: disable=unsupported-membership-test,unsubscriptable-object + if specified_attribute_name in entity_attribute_names: + # pylint: disable=no-member + return entity_full_list[entity_attribute_names.index(specified_attribute_name)] + raise ValueError( + f"The given attribute_name {attribute_name} is not found" + f" in geometry metadata. Available: {entity_attribute_names}" + ) def get_boundaries(self, attribute_name: str = None) -> list: """ - Get the full list of boundary names. If it is geometry then use supplied attribute name to get the list. + Get the full list of boundaries. + If attribute_name is supplied then ignore stored face_group_tag and use supplied one. """ return self._get_list_of_entities(attribute_name, "face") @@ -144,12 +151,24 @@ class VolumeMeshEntityInfo(EntityInfoModel): zones: list[GenericVolume] = pd.Field([]) boundaries: list[Surface] = pd.Field([]) + @pd.field_validator("boundaries", mode="after") + @classmethod + def check_all_surface_has_interface_indicator(cls, value): + """private_attribute_is_interface should have been set comming from volume mesh.""" + for item in value: + if item.private_attribute_is_interface is None: + raise ValueError( + "[INTERNAL] {item.name} is missing private_attribute_is_interface attribute!." + ) + return value + # pylint: disable=arguments-differ def get_boundaries(self) -> list: """ Get the full list of boundary names. If it is geometry then use supplied attribute name to get the list. """ - return self.boundaries + # pylint: disable=not-an-iterable + return [item for item in self.boundaries if item.private_attribute_is_interface is False] class SurfaceMeshEntityInfo(EntityInfoModel): diff --git a/flow360/component/simulation/framework/base_model.py b/flow360/component/simulation/framework/base_model.py index b3f84bf35..0336d784e 100644 --- a/flow360/component/simulation/framework/base_model.py +++ b/flow360/component/simulation/framework/base_model.py @@ -163,6 +163,7 @@ def one_of(cls, values): # pylint: disable=no-self-argument # pylint: disable=duplicate-code @pd.model_validator(mode="before") + @classmethod def handle_conflicting_fields(cls, values): """ root validator to handle deprecated aliases and fields diff --git a/flow360/component/simulation/framework/param_utils.py b/flow360/component/simulation/framework/param_utils.py index c45b74a39..0f89a71ce 100644 --- a/flow360/component/simulation/framework/param_utils.py +++ b/flow360/component/simulation/framework/param_utils.py @@ -32,6 +32,15 @@ class AssetCache(Flow360BaseModel): Union[GeometryEntityInfo, VolumeMeshEntityInfo, SurfaceMeshEntityInfo] ] = pd.Field(None, frozen=True, discriminator="type_name") + @property + def boundaries(self): + """ + Get all boundaries from the cached entity info. + """ + if self.project_entity_info is None: + return None + return self.project_entity_info.get_boundaries() + def register_entity_list(model: Flow360BaseModel, registry: EntityRegistry) -> None: """ diff --git a/flow360/component/simulation/operating_condition/operating_condition.py b/flow360/component/simulation/operating_condition/operating_condition.py index 6835a7616..aca9103ba 100644 --- a/flow360/component/simulation/operating_condition/operating_condition.py +++ b/flow360/component/simulation/operating_condition/operating_condition.py @@ -230,8 +230,8 @@ def from_mach( reference_velocity_magnitude=reference_velocity_magnitude, ) - @context_validator(context=CASE) @pd.model_validator(mode="after") + @context_validator(context=CASE) def check_valid_reference_velocity(self) -> Self: """Ensure reference velocity is provided when freestream velocity is 0.""" if self.velocity_magnitude.value == 0 and self.reference_velocity_magnitude is None: diff --git a/flow360/component/simulation/simulation_params.py b/flow360/component/simulation/simulation_params.py index 8e36babc1..2c9232a06 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -44,6 +44,7 @@ ) from flow360.component.simulation.validation.validation_simulation_params import ( _check_cht_solver_settings, + _check_complete_boundary_condition_and_unknown_surface, _check_consistency_ddes_volume_output, _check_consistency_wall_function_and_surface_output, _check_low_mach_preconditioner_output, @@ -56,7 +57,13 @@ from flow360.exceptions import Flow360ConfigurationError, Flow360RuntimeError from flow360.version import __version__ -from .validation.validation_context import SURFACE_MESH, CaseField, ContextField +from .validation.validation_context import ( + CASE, + SURFACE_MESH, + CaseField, + ContextField, + context_validator, +) ModelTypes = Annotated[Union[VolumeModelTypes, SurfaceModelTypes], pd.Field(discriminator="type")] @@ -242,34 +249,35 @@ def apply_defult_output_settings(cls, v): return v @pd.model_validator(mode="after") - @classmethod - def check_cht_solver_settings(cls, params): + def check_cht_solver_settings(self): """Check the Conjugate Heat Transfer settings, transferred from checkCHTSolverSettings""" - return _check_cht_solver_settings(params) + return _check_cht_solver_settings(self) @pd.model_validator(mode="after") - @classmethod - def check_consistency_wall_function_and_surface_output(cls, v): + def check_consistency_wall_function_and_surface_output(self): """Only allow wallFunctionMetric output field when there is a Wall model with a wall function enabled""" - return _check_consistency_wall_function_and_surface_output(v) + return _check_consistency_wall_function_and_surface_output(self) @pd.model_validator(mode="after") - @classmethod - def check_consistency_ddes_volume_output(cls, v): + def check_consistency_ddes_volume_output(self): """Only allow DDES output field when there is a corresponding solver with DDES enabled in models""" - return _check_consistency_ddes_volume_output(v) + return _check_consistency_ddes_volume_output(self) @pd.model_validator(mode="after") - @classmethod - def check_numerical_dissipation_factor_output(cls, v): + def check_numerical_dissipation_factor_output(self): """Only allow numericalDissipationFactor output field when the NS solver has low numerical dissipation""" - return _check_numerical_dissipation_factor_output(v) + return _check_numerical_dissipation_factor_output(self) @pd.model_validator(mode="after") - @classmethod - def check_low_mach_preconditioner_output(cls, v): + def check_low_mach_preconditioner_output(self): """Only allow lowMachPreconditioner output field when the lowMachPreconditioner is enabled in the NS solver""" - return _check_low_mach_preconditioner_output(v) + return _check_low_mach_preconditioner_output(self) + + @pd.model_validator(mode="after") + @context_validator(context=CASE) + def check_complete_boundary_condition_and_unknown_surface(self): + """Make sure that all boundaries have been assigned with a boundary condition""" + return _check_complete_boundary_condition_and_unknown_surface(self) def _move_registry_to_asset_cache(self, registry: EntityRegistry) -> EntityRegistry: """Recursively register all entities listed in EntityList to the asset cache.""" diff --git a/flow360/component/simulation/translator/surface_meshing_translator.py b/flow360/component/simulation/translator/surface_meshing_translator.py index 927b4ef7f..41b0b4d6d 100644 --- a/flow360/component/simulation/translator/surface_meshing_translator.py +++ b/flow360/component/simulation/translator/surface_meshing_translator.py @@ -127,11 +127,8 @@ def get_surface_meshing_json(input_params: SimulationParams, mesh_units): ##:: >> Step 6: Tell surface mesher how do we group boundaries. translated["boundaries"] = {} - face_group_tag = input_params.private_attribute_asset_cache.project_entity_info.face_group_tag grouped_faces: List[Surface] = ( - input_params.private_attribute_asset_cache.project_entity_info.get_boundaries( - face_group_tag - ) + input_params.private_attribute_asset_cache.project_entity_info.get_boundaries() ) for surface in grouped_faces: for face_id in surface.private_attribute_sub_components: diff --git a/flow360/component/simulation/validation/validation_context.py b/flow360/component/simulation/validation/validation_context.py index 2b36648c8..a16172f23 100644 --- a/flow360/component/simulation/validation/validation_context.py +++ b/flow360/component/simulation/validation/validation_context.py @@ -176,7 +176,7 @@ def context_validator(context: Literal["SurfaceMesh", "VolumeMesh", "Case"]): Notes ----- - This decorator is designed to be used with Pydantic model validators to ensure that + This decorator is designed to be used with Pydantic **model** validators to ensure that certain validations are only executed when the validation level matches the given context. """ @@ -185,7 +185,7 @@ def decorator(func: Callable): def wrapper(self: Any, *args, **kwargs): current_levels = get_validation_levels() # Run the validator only if the current levels matches the specified context or is ALL - if current_levels is None or any(lvl in (context, ALL) for lvl in current_levels): + if current_levels is not None and any(lvl in (context, ALL) for lvl in current_levels): return func(self, *args, **kwargs) return self diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index e56d82895..e8cf29b91 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -2,8 +2,11 @@ validation for SimulationParams """ +from typing import get_args + +from flow360.component.simulation.framework.entity_registry import EntityRegistry from flow360.component.simulation.models.solver_numerics import NoneSolver -from flow360.component.simulation.models.surface_models import Wall +from flow360.component.simulation.models.surface_models import SurfaceModelTypes, Wall from flow360.component.simulation.models.volume_models import ( Fluid, NavierStokesInitialCondition, @@ -16,6 +19,7 @@ SurfaceOutput, VolumeOutput, ) +from flow360.component.simulation.primitives import Surface from flow360.component.simulation.time_stepping.time_stepping import Unsteady @@ -220,3 +224,47 @@ def _validate_cht_has_heat_transfer(params): ) return params + + +def _check_complete_boundary_condition_and_unknown_surface(params): + ##:: Step 1: Get all boundaries patches from asset cache + asset_boundaries: list[Surface] = params.private_attribute_asset_cache.boundaries + if asset_boundaries is None: + # No entity info found. Skip the validation. + return params + ##:: Step 2: Make sure that all boundaries appear in the models section. + if params.models is None: + raise ValueError("To run a case, `models` field must be defined.") + param_boundary_registry = EntityRegistry() + + for model in params.models: + if not isinstance(model, get_args(SurfaceModelTypes)): + # None-BC models + continue + # Get referenced entities + if hasattr(model, "entities"): + # pylint: disable=protected-access + for entity in model.entities._get_expanded_entities(create_hard_copy=False): + param_boundary_registry.register(entity) + elif hasattr(model, "entity_pairs"): # Periodic BC + for surface_pair in model.entity_pairs.items: + param_boundary_registry.register(surface_pair.pair[0]) + param_boundary_registry.register(surface_pair.pair[1]) + + for boundary in asset_boundaries: + if param_boundary_registry.contains(boundary) is False: + raise ValueError( + f"{boundary.name} does not have a boundary condition. Please add to it a model in the `models` section." + ) + + ##:: Step 3: + ##:: class `GhostSurface` (mostly just farfield/symmetry) + ##:: can be assigned with BC but may/may not be in the entity info. + ##:: Therefore we do not include them in the `param_boundaries`. + param_boundaries = param_boundary_registry.find_by_type(Surface) + for boundary in param_boundaries: + if boundary not in asset_boundaries: + raise ValueError( + f"{boundary.name} is not a known `Surface` entity but it appears in the `models` section." + ) + return params diff --git a/tests/simulation/params/test_validators_params.py b/tests/simulation/params/test_validators_params.py index bfa9a7f5a..f79e44753 100644 --- a/tests/simulation/params/test_validators_params.py +++ b/tests/simulation/params/test_validators_params.py @@ -1,11 +1,22 @@ +import json import re import unittest +from typing import Literal import pytest import flow360.component.simulation.units as u +from flow360.component.simulation.entity_info import VolumeMeshEntityInfo +from flow360.component.simulation.framework.param_utils import AssetCache +from flow360.component.simulation.meshing_param.volume_params import AutomatedFarfield from flow360.component.simulation.models.material import SolidMaterial, aluminum -from flow360.component.simulation.models.surface_models import Wall +from flow360.component.simulation.models.surface_models import ( + Freestream, + Periodic, + SlipWall, + Translational, + Wall, +) from flow360.component.simulation.models.volume_models import ( Fluid, HeatEquationInitialCondition, @@ -14,9 +25,17 @@ ) from flow360.component.simulation.outputs.outputs import SurfaceOutput, VolumeOutput from flow360.component.simulation.primitives import GenericVolume, Surface +from flow360.component.simulation.services import validate_model from flow360.component.simulation.simulation_params import SimulationParams from flow360.component.simulation.time_stepping.time_stepping import Steady, Unsteady from flow360.component.simulation.unit_system import SI_unit_system +from flow360.component.simulation.validation.validation_context import ( + ALL, + CASE, + SURFACE_MESH, + VOLUME_MESH, + ValidationLevelContext, +) assertions = unittest.TestCase("__init__") @@ -279,3 +298,70 @@ def test_cht_solver_settings_validator( time_stepping=Steady(), outputs=[surface_output_with_residual_heat_solver], ) + + +def test_incomplete_BC(): + ##:: Construct a dummy asset cache + + wall_1 = Surface( + name="wall_1", private_attribute_is_interface=False, private_attribute_tag_key="test" + ) + periodic_1 = Surface( + name="periodic_1", private_attribute_is_interface=False, private_attribute_tag_key="test" + ) + periodic_2 = Surface( + name="periodic_2", private_attribute_is_interface=False, private_attribute_tag_key="test" + ) + i_exist = Surface( + name="i_exist", private_attribute_is_interface=False, private_attribute_tag_key="test" + ) + no_bc = Surface( + name="no_bc", private_attribute_is_interface=False, private_attribute_tag_key="test" + ) + some_interface = Surface( + name="some_interface", private_attribute_is_interface=True, private_attribute_tag_key="test" + ) + + asset_cache = AssetCache( + project_length_unit="inch", + project_entity_info=VolumeMeshEntityInfo( + boundaries=[wall_1, periodic_1, periodic_2, i_exist, some_interface, no_bc] + ), + ) + auto_farfield = AutomatedFarfield(name="my_farfield") + + with SI_unit_system: + param = SimulationParams( + models=[ + Fluid(), + Wall(entities=wall_1), + Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational()), + SlipWall(entities=[i_exist]), + Freestream(entities=auto_farfield.farfield), + ], + private_attribute_asset_cache=asset_cache, + ) + + def _validate_under_CASE(param): + param_dict = json.loads(param.model_dump_json()) + with ValidationLevelContext(CASE): + with SI_unit_system: + SimulationParams.model_validate(param_dict) + + with pytest.raises( + ValueError, + match=re.escape( + r"no_bc does not have a boundary condition. Please add to it a model in the `models` section." + ), + ): + _validate_under_CASE(param) + + param.models.append(SlipWall(entities=[Surface(name="plz_dont_do_this"), no_bc])) + + with pytest.raises( + ValueError, + match=re.escape( + r"plz_dont_do_this is not a known `Surface` entity but it appears in the `models` section." + ), + ): + _validate_under_CASE(param) From 1f93112818e985d64cddd7a637ee1d07332f031d Mon Sep 17 00:00:00 2001 From: BenYuan Date: Fri, 25 Oct 2024 19:07:20 +0000 Subject: [PATCH 2/9] Fixing unit test --- .../validation/validation_context.py | 2 +- .../validation_simulation_params.py | 73 ++++++++++--------- .../params/test_validators_params.py | 50 ++++++------- 3 files changed, 66 insertions(+), 59 deletions(-) diff --git a/flow360/component/simulation/validation/validation_context.py b/flow360/component/simulation/validation/validation_context.py index a16172f23..a0c958d5e 100644 --- a/flow360/component/simulation/validation/validation_context.py +++ b/flow360/component/simulation/validation/validation_context.py @@ -185,7 +185,7 @@ def decorator(func: Callable): def wrapper(self: Any, *args, **kwargs): current_levels = get_validation_levels() # Run the validator only if the current levels matches the specified context or is ALL - if current_levels is not None and any(lvl in (context, ALL) for lvl in current_levels): + if current_levels is None or any(lvl in (context, ALL) for lvl in current_levels): return func(self, *args, **kwargs) return self diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index d633e5953..0b6231d7c 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -4,7 +4,6 @@ from typing import get_args -from flow360.component.simulation.framework.entity_registry import EntityRegistry from flow360.component.simulation.models.solver_numerics import NoneSolver from flow360.component.simulation.models.surface_models import SurfaceModelTypes, Wall from flow360.component.simulation.models.volume_models import ( @@ -20,6 +19,7 @@ VolumeOutput, ) from flow360.component.simulation.primitives import ( + GhostSurface, Surface, _SurfaceEntityBase, _VolumeEntityBase, @@ -271,44 +271,51 @@ def _validate_cht_has_heat_transfer(params): def _check_complete_boundary_condition_and_unknown_surface(params): - ##:: Step 1: Get all boundaries patches from asset cache - asset_boundaries: list[Surface] = params.private_attribute_asset_cache.boundaries - if asset_boundaries is None: - # No entity info found. Skip the validation. + print(">>>>Running...") + ## Step 1: Get all boundaries patches from asset cache + asset_boundary_entities = params.private_attribute_asset_cache.boundaries + if asset_boundary_entities is None: return params - ##:: Step 2: Make sure that all boundaries appear in the models section. - if params.models is None: - raise ValueError("To run a case, `models` field must be defined.") - param_boundary_registry = EntityRegistry() + asset_boundaries = {boundary.name for boundary in asset_boundary_entities} + + ## Step 2: Collect all used boundaries from the models + if len(params.models) == 1 and isinstance(params.models[0], Fluid): + return params + + used_boundaries = set() for model in params.models: if not isinstance(model, get_args(SurfaceModelTypes)): - # None-BC models continue - # Get referenced entities + + entities = [] if hasattr(model, "entities"): - # pylint: disable=protected-access - for entity in model.entities._get_expanded_entities(create_hard_copy=False): - param_boundary_registry.register(entity) + entities = model.entities._get_expanded_entities(create_hard_copy=False) elif hasattr(model, "entity_pairs"): # Periodic BC - for surface_pair in model.entity_pairs.items: - param_boundary_registry.register(surface_pair.pair[0]) - param_boundary_registry.register(surface_pair.pair[1]) + entities = [ + pair for surface_pair in model.entity_pairs.items for pair in surface_pair.pair + ] + + for entity in entities: + if isinstance(entity, GhostSurface): + continue + used_boundaries.add(entity.name) + + ## Step 3: Use set operations to find missing and unknown boundaries + missing_boundaries = asset_boundaries - used_boundaries + unknown_boundaries = used_boundaries - asset_boundaries + + if missing_boundaries: + missing_list = ", ".join(sorted(missing_boundaries)) + raise ValueError( + f"The following boundaries do not have a boundary condition: {missing_list}. " + "Please add them to a model in the `models` section." + ) + + if unknown_boundaries: + unknown_list = ", ".join(sorted(unknown_boundaries)) + raise ValueError( + f"The following boundaries are not known `Surface` entities but appear in the `models` section: {unknown_list}." + ) - for boundary in asset_boundaries: - if param_boundary_registry.contains(boundary) is False: - raise ValueError( - f"{boundary.name} does not have a boundary condition. Please add to it a model in the `models` section." - ) - - ##:: Step 3: - ##:: class `GhostSurface` (mostly just farfield/symmetry) - ##:: can be assigned with BC but may/may not be in the entity info. - ##:: Therefore we do not include them in the `param_boundaries`. - param_boundaries = param_boundary_registry.find_by_type(Surface) - for boundary in param_boundaries: - if boundary not in asset_boundaries: - raise ValueError( - f"{boundary.name} is not a known `Surface` entity but it appears in the `models` section." - ) return params diff --git a/tests/simulation/params/test_validators_params.py b/tests/simulation/params/test_validators_params.py index c13efbc6c..782e03bd5 100644 --- a/tests/simulation/params/test_validators_params.py +++ b/tests/simulation/params/test_validators_params.py @@ -342,41 +342,41 @@ def test_incomplete_BC(): ) auto_farfield = AutomatedFarfield(name="my_farfield") - with SI_unit_system: - param = SimulationParams( - models=[ - Fluid(), - Wall(entities=wall_1), - Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational()), - SlipWall(entities=[i_exist]), - Freestream(entities=auto_farfield.farfield), - ], - private_attribute_asset_cache=asset_cache, - ) - - def _validate_under_CASE(param): - param_dict = json.loads(param.model_dump_json()) - with ValidationLevelContext(CASE): - with SI_unit_system: - SimulationParams.model_validate(param_dict) - with pytest.raises( ValueError, match=re.escape( - r"no_bc does not have a boundary condition. Please add to it a model in the `models` section." + r"The following boundaries do not have a boundary condition: no_bc. Please add them to a model in the `models` section." ), ): - _validate_under_CASE(param) - - param.models.append(SlipWall(entities=[Surface(name="plz_dont_do_this"), no_bc])) - + with SI_unit_system: + SimulationParams( + models=[ + Fluid(), + Wall(entities=wall_1), + Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational()), + SlipWall(entities=[i_exist]), + Freestream(entities=auto_farfield.farfield), + ], + private_attribute_asset_cache=asset_cache, + ) with pytest.raises( ValueError, match=re.escape( - r"plz_dont_do_this is not a known `Surface` entity but it appears in the `models` section." + r"The following boundaries are not known `Surface` entities but appear in the `models` section: plz_dont_do_this." ), ): - _validate_under_CASE(param) + with SI_unit_system: + SimulationParams( + models=[ + Fluid(), + Wall(entities=[wall_1]), + Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational()), + SlipWall(entities=[i_exist]), + Freestream(entities=auto_farfield.farfield), + SlipWall(entities=[Surface(name="plz_dont_do_this"), no_bc]), + ], + private_attribute_asset_cache=asset_cache, + ) def test_duplicate_entities_in_models(): From 71f575c898d3a7b052f901d3e3de9132dd9cf10b Mon Sep 17 00:00:00 2001 From: BenYuan Date: Fri, 25 Oct 2024 21:07:51 +0000 Subject: [PATCH 3/9] Fixed unit test --- .../operating_condition/operating_condition.py | 6 +++++- .../simulation/validation/validation_context.py | 2 +- .../validation/validation_simulation_params.py | 6 +++--- tests/simulation/params/test_validators_params.py | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/flow360/component/simulation/operating_condition/operating_condition.py b/flow360/component/simulation/operating_condition/operating_condition.py index aca9103ba..501e1e97a 100644 --- a/flow360/component/simulation/operating_condition/operating_condition.py +++ b/flow360/component/simulation/operating_condition/operating_condition.py @@ -234,7 +234,11 @@ def from_mach( @context_validator(context=CASE) def check_valid_reference_velocity(self) -> Self: """Ensure reference velocity is provided when freestream velocity is 0.""" - if self.velocity_magnitude.value == 0 and self.reference_velocity_magnitude is None: + if ( + self.velocity_magnitude is not None + and self.velocity_magnitude.value == 0 + and self.reference_velocity_magnitude is None + ): raise ValueError( "Reference velocity magnitude/Mach must be provided when freestream velocity magnitude/Mach is 0." ) diff --git a/flow360/component/simulation/validation/validation_context.py b/flow360/component/simulation/validation/validation_context.py index a0c958d5e..2b36648c8 100644 --- a/flow360/component/simulation/validation/validation_context.py +++ b/flow360/component/simulation/validation/validation_context.py @@ -176,7 +176,7 @@ def context_validator(context: Literal["SurfaceMesh", "VolumeMesh", "Case"]): Notes ----- - This decorator is designed to be used with Pydantic **model** validators to ensure that + This decorator is designed to be used with Pydantic model validators to ensure that certain validations are only executed when the validation level matches the given context. """ diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index 0b6231d7c..06128a3ff 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -271,10 +271,9 @@ def _validate_cht_has_heat_transfer(params): def _check_complete_boundary_condition_and_unknown_surface(params): - print(">>>>Running...") ## Step 1: Get all boundaries patches from asset cache asset_boundary_entities = params.private_attribute_asset_cache.boundaries - if asset_boundary_entities is None: + if asset_boundary_entities is None or asset_boundary_entities == []: return params asset_boundaries = {boundary.name for boundary in asset_boundary_entities} @@ -315,7 +314,8 @@ def _check_complete_boundary_condition_and_unknown_surface(params): if unknown_boundaries: unknown_list = ", ".join(sorted(unknown_boundaries)) raise ValueError( - f"The following boundaries are not known `Surface` entities but appear in the `models` section: {unknown_list}." + f"The following boundaries are not known `Surface` " + f"entities but appear in the `models` section: {unknown_list}." ) return params diff --git a/tests/simulation/params/test_validators_params.py b/tests/simulation/params/test_validators_params.py index 782e03bd5..5f23646f8 100644 --- a/tests/simulation/params/test_validators_params.py +++ b/tests/simulation/params/test_validators_params.py @@ -23,6 +23,9 @@ NavierStokesInitialCondition, Solid, ) +from flow360.component.simulation.operating_condition.operating_condition import ( + AerospaceCondition, +) from flow360.component.simulation.outputs.outputs import SurfaceOutput, VolumeOutput from flow360.component.simulation.primitives import GenericVolume, Surface from flow360.component.simulation.services import validate_model @@ -410,3 +413,14 @@ def test_duplicate_entities_in_models(): _ = SimulationParams( models=[volume_model1, volume_model2, surface_model1, surface_model2, surface_model3], ) + + +def test_valid_reference_velocity(): + with pytest.raises( + ValueError, + match=re.escape( + "Reference velocity magnitude/Mach must be provided when freestream velocity magnitude/Mach is 0." + ), + ): + with SI_unit_system: + SimulationParams(operating_condition=AerospaceCondition(velocity_magnitude=0)) From 971359b892f2125fa2ae3d1e32ca416efd242112 Mon Sep 17 00:00:00 2001 From: BenYuan Date: Fri, 25 Oct 2024 21:09:18 +0000 Subject: [PATCH 4/9] Fix lint --- .../simulation/validation/validation_simulation_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index 06128a3ff..1bfcb19ad 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -20,7 +20,6 @@ ) from flow360.component.simulation.primitives import ( GhostSurface, - Surface, _SurfaceEntityBase, _VolumeEntityBase, ) @@ -288,6 +287,7 @@ def _check_complete_boundary_condition_and_unknown_surface(params): continue entities = [] + # pylint: disable=protected-access if hasattr(model, "entities"): entities = model.entities._get_expanded_entities(create_hard_copy=False) elif hasattr(model, "entity_pairs"): # Periodic BC From 0d7e8803482fe3dc0c20457596799dada581f454 Mon Sep 17 00:00:00 2001 From: Ben <106089368+benflexcompute@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:05:46 -0400 Subject: [PATCH 5/9] Update flow360/component/simulation/validation/validation_simulation_params.py Co-authored-by: Maciej Skarysz <83596707+maciej-flexcompute@users.noreply.github.com> --- .../simulation/validation/validation_simulation_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index 1bfcb19ad..d283d6298 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -274,7 +274,7 @@ def _check_complete_boundary_condition_and_unknown_surface(params): asset_boundary_entities = params.private_attribute_asset_cache.boundaries if asset_boundary_entities is None or asset_boundary_entities == []: return params - asset_boundaries = {boundary.name for boundary in asset_boundary_entities} + asset_boundaries = set(asset_boundary_entities) ## Step 2: Collect all used boundaries from the models if len(params.models) == 1 and isinstance(params.models[0], Fluid): From c9ecb85a620c410bde288659b1a0a8ba6b896282 Mon Sep 17 00:00:00 2001 From: Ben <106089368+benflexcompute@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:07:55 -0400 Subject: [PATCH 6/9] Update flow360/component/simulation/validation/validation_simulation_params.py Co-authored-by: Maciej Skarysz <83596707+maciej-flexcompute@users.noreply.github.com> --- .../simulation/validation/validation_simulation_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index d283d6298..dc8fa3fce 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -308,7 +308,7 @@ def _check_complete_boundary_condition_and_unknown_surface(params): missing_list = ", ".join(sorted(missing_boundaries)) raise ValueError( f"The following boundaries do not have a boundary condition: {missing_list}. " - "Please add them to a model in the `models` section." + "Please add them to a boundary condition model in the `models` section." ) if unknown_boundaries: From 9c234a1f4c55da28d0c78f999e103cc9ffc62540 Mon Sep 17 00:00:00 2001 From: BenYuan Date: Thu, 31 Oct 2024 18:46:19 +0000 Subject: [PATCH 7/9] Fixed the unit test --- .../simulation/validation/validation_simulation_params.py | 2 +- tests/simulation/params/test_validators_params.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index dc8fa3fce..c3a0af6d8 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -274,7 +274,7 @@ def _check_complete_boundary_condition_and_unknown_surface(params): asset_boundary_entities = params.private_attribute_asset_cache.boundaries if asset_boundary_entities is None or asset_boundary_entities == []: return params - asset_boundaries = set(asset_boundary_entities) + asset_boundaries = {boundary.name for boundary in asset_boundary_entities} ## Step 2: Collect all used boundaries from the models if len(params.models) == 1 and isinstance(params.models[0], Fluid): diff --git a/tests/simulation/params/test_validators_params.py b/tests/simulation/params/test_validators_params.py index 5f23646f8..dbea2d886 100644 --- a/tests/simulation/params/test_validators_params.py +++ b/tests/simulation/params/test_validators_params.py @@ -348,7 +348,7 @@ def test_incomplete_BC(): with pytest.raises( ValueError, match=re.escape( - r"The following boundaries do not have a boundary condition: no_bc. Please add them to a model in the `models` section." + r"The following boundaries do not have a boundary condition: no_bc. Please add them to a boundary condition model in the `models` section." ), ): with SI_unit_system: From ecf238003c23c9fd0ae6bfab25936060df4a3269 Mon Sep 17 00:00:00 2001 From: BenYuan Date: Thu, 7 Nov 2024 22:09:40 +0000 Subject: [PATCH 8/9] FIX UNIT TEST --- flow360/component/simulation/simulation_params.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flow360/component/simulation/simulation_params.py b/flow360/component/simulation/simulation_params.py index 8be41915c..d515ce38d 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -169,6 +169,7 @@ def copy(self, update=None, **kwargs) -> _ParamModelBase: return super().copy(update=update, **kwargs) +# pylint: disable=too-many-public-methods class SimulationParams(_ParamModelBase): """ meshing (Optional[MeshingParams]): Contains all the user specified meshing parameters that either enrich or From 093e708c41d0dfaeb69b4f0a25d93dec3b214efa Mon Sep 17 00:00:00 2001 From: BenYuan Date: Fri, 8 Nov 2024 16:45:57 +0000 Subject: [PATCH 9/9] Added guard on the validation level --- .../validation_simulation_params.py | 11 +- ...tion_json_with_multi_constructor_used.json | 53 +++++++- .../params/test_validators_params.py | 57 ++++---- tests/simulation/service/test_services_v2.py | 128 +++++++++++++++++- .../service/test_translator_service.py | 124 ++++++++++++++++- 5 files changed, 334 insertions(+), 39 deletions(-) diff --git a/flow360/component/simulation/validation/validation_simulation_params.py b/flow360/component/simulation/validation/validation_simulation_params.py index 29d4f3bf5..7798527e9 100644 --- a/flow360/component/simulation/validation/validation_simulation_params.py +++ b/flow360/component/simulation/validation/validation_simulation_params.py @@ -277,14 +277,21 @@ def _validate_cht_has_heat_transfer(params): def _check_complete_boundary_condition_and_unknown_surface(params): ## Step 1: Get all boundaries patches from asset cache + + current_lvls = get_validation_levels() if get_validation_levels() else [] + if all(level not in current_lvls for level in (ALL, CASE)): + return params + asset_boundary_entities = params.private_attribute_asset_cache.boundaries + if asset_boundary_entities is None or asset_boundary_entities == []: - return params + raise ValueError("[Internal] Failed to retrieve asset boundaries") + asset_boundaries = {boundary.name for boundary in asset_boundary_entities} ## Step 2: Collect all used boundaries from the models if len(params.models) == 1 and isinstance(params.models[0], Fluid): - return params + raise ValueError("No boundary conditions are defined in the `models` section.") used_boundaries = set() diff --git a/tests/ref/simulation/simulation_json_with_multi_constructor_used.json b/tests/ref/simulation/simulation_json_with_multi_constructor_used.json index dab3a67c9..a038cecc2 100644 --- a/tests/ref/simulation/simulation_json_with_multi_constructor_used.json +++ b/tests/ref/simulation/simulation_json_with_multi_constructor_used.json @@ -183,6 +183,25 @@ ] }, "models": [ + { + "type": "Wall", + "entities": { + "stored_entities": [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "surface_x", + "private_attribute_is_interface": false, + "private_attribute_sub_components": [ + "face_x_1", + "face_x_2", + "face_x_3" + ] + } + ] + }, + "use_wall_function": false + }, { "material": { "dynamic_viscosity": { @@ -368,5 +387,35 @@ "unit_system": { "name": "SI" }, - "version": "24.2.0" -} + "version": "24.2.0", + "private_attribute_asset_cache": { + "project_length_unit": "m", + "project_entity_info": { + "type_name": "GeometryEntityInfo", + "face_ids": [ + "face_x_1", + "face_x_2", + "face_x_3" + ], + "face_group_tag": "some_tag", + "face_attribute_names": [ + "some_tag" + ], + "grouped_faces": [ + [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "surface_x", + "private_attribute_is_interface": false, + "private_attribute_sub_components": [ + "face_x_1", + "face_x_2", + "face_x_3" + ] + } + ] + ] + } + } +} \ No newline at end of file diff --git a/tests/simulation/params/test_validators_params.py b/tests/simulation/params/test_validators_params.py index 17728e18e..c8b52820b 100644 --- a/tests/simulation/params/test_validators_params.py +++ b/tests/simulation/params/test_validators_params.py @@ -360,35 +360,37 @@ def test_incomplete_BC(): r"The following boundaries do not have a boundary condition: no_bc. Please add them to a boundary condition model in the `models` section." ), ): - with SI_unit_system: - SimulationParams( - models=[ - Fluid(), - Wall(entities=wall_1), - Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational()), - SlipWall(entities=[i_exist]), - Freestream(entities=auto_farfield.farfield), - ], - private_attribute_asset_cache=asset_cache, - ) + with ValidationLevelContext(ALL): + with SI_unit_system: + SimulationParams( + models=[ + Fluid(), + Wall(entities=wall_1), + Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational()), + SlipWall(entities=[i_exist]), + Freestream(entities=auto_farfield.farfield), + ], + private_attribute_asset_cache=asset_cache, + ) with pytest.raises( ValueError, match=re.escape( r"The following boundaries are not known `Surface` entities but appear in the `models` section: plz_dont_do_this." ), ): - with SI_unit_system: - SimulationParams( - models=[ - Fluid(), - Wall(entities=[wall_1]), - Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational()), - SlipWall(entities=[i_exist]), - Freestream(entities=auto_farfield.farfield), - SlipWall(entities=[Surface(name="plz_dont_do_this"), no_bc]), - ], - private_attribute_asset_cache=asset_cache, - ) + with ValidationLevelContext(ALL): + with SI_unit_system: + SimulationParams( + models=[ + Fluid(), + Wall(entities=[wall_1]), + Periodic(surface_pairs=(periodic_1, periodic_2), spec=Translational()), + SlipWall(entities=[i_exist]), + Freestream(entities=auto_farfield.farfield), + SlipWall(entities=[Surface(name="plz_dont_do_this"), no_bc]), + ], + private_attribute_asset_cache=asset_cache, + ) def test_duplicate_entities_in_models(): @@ -539,6 +541,8 @@ def test_rotation_parent_volumes(): axis=(0, 1, 2), ) + my_wall = Surface(name="my_wall", private_attribute_is_interface=False) + msg = "For model #1, the parent rotating volume (stationary_cylinder) is not " "used in any other `Rotation` model's `volumes`." with pytest.raises(ValueError, match=re.escape(msg)): @@ -558,5 +562,10 @@ def test_rotation_parent_volumes(): Fluid(), Rotation(entities=[c_1], spec=AngleExpression("1+2"), parent_volume=c_2), Rotation(entities=[c_2], spec=AngleExpression("1+5")), - ] + Wall(entities=[my_wall]), + ], + private_attribute_asset_cache=AssetCache( + project_length_unit="cm", + project_entity_info=VolumeMeshEntityInfo(boundaries=[my_wall]), + ), ) diff --git a/tests/simulation/service/test_services_v2.py b/tests/simulation/service/test_services_v2.py index 1d74f9f6a..bb2e41879 100644 --- a/tests/simulation/service/test_services_v2.py +++ b/tests/simulation/service/test_services_v2.py @@ -3,8 +3,9 @@ import pytest from flow360.component.simulation import services -from flow360.component.simulation.simulation_params import SimulationParams -from flow360.component.simulation.unit_system import SI_unit_system +from flow360.component.simulation.entity_info import VolumeMeshEntityInfo +from flow360.component.simulation.framework.param_utils import AssetCache +from flow360.component.simulation.primitives import Surface from flow360.component.simulation.validation.validation_context import ( SURFACE_MESH, VOLUME_MESH, @@ -50,7 +51,43 @@ def test_validate_service(): "max_steps": 10, "CFL": {"type": "ramp", "initial": 1.5, "final": 1.5, "ramp_steps": 5}, }, + "models": [ + { + "type": "Wall", + "entities": { + "stored_entities": [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "Mysurface", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [], + } + ] + }, + "use_wall_function": False, + } + ], "user_defined_dynamics": [], + "private_attribute_asset_cache": { + "project_length_unit": None, + "project_entity_info": { + "draft_entities": [], + "type_name": "VolumeMeshEntityInfo", + "zones": [], + "boundaries": [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "Mysurface", + "private_attribute_full_name": None, + "private_attribute_is_interface": False, + "private_attribute_tag_key": None, + "private_attribute_sub_components": [], + } + ], + }, + }, } params_data_from_geo = params_data_from_vm @@ -424,6 +461,51 @@ def test_front_end_JSON_with_multi_constructor(): }, "unit_system": {"name": "SI"}, "version": "24.2.0", + "private_attribute_asset_cache": { + "project_length_unit": "m", + "project_entity_info": { + "type_name": "GeometryEntityInfo", + "face_ids": ["face_x_1", "face_x_2", "face_x_3"], + "face_group_tag": "some_tag", + "face_attribute_names": ["some_tag"], + "grouped_faces": [ + [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "surface_x", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_1", + "face_x_2", + "face_x_3", + ], + } + ] + ], + }, + }, + "models": [ + { + "type": "Wall", + "entities": { + "stored_entities": [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "surface_x", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_1", + "face_x_2", + "face_x_3", + ], + } + ] + }, + "use_wall_function": False, + } + ], "operating_condition": { "type_name": "AerospaceCondition", "private_attribute_constructor": "from_mach", @@ -489,13 +571,49 @@ def test_generate_process_json(): "beta": {"value": 0.0, "units": "degree"}, # "velocity_magnitude": {"value": 0.8, "units": "km/s"}, }, + "models": [ + { + "type": "Wall", + "entities": { + "stored_entities": [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "surface_x", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_1", + "face_x_2", + "face_x_3", + ], + } + ] + }, + "use_wall_function": False, + } + ], "private_attribute_asset_cache": { "project_length_unit": "m", "project_entity_info": { "type_name": "GeometryEntityInfo", - "face_ids": ["face_x"], - "face_group_tag": "not_used", - "face_attribute_names": ["not_used"], + "face_ids": ["face_x_1", "face_x_2", "face_x_3"], + "face_group_tag": "some_tag", + "face_attribute_names": ["some_tag"], + "grouped_faces": [ + [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "surface_x", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_1", + "face_x_2", + "face_x_3", + ], + } + ] + ], }, }, } diff --git a/tests/simulation/service/test_translator_service.py b/tests/simulation/service/test_translator_service.py index df0aa2439..f553422c8 100644 --- a/tests/simulation/service/test_translator_service.py +++ b/tests/simulation/service/test_translator_service.py @@ -397,6 +397,46 @@ def test_simulation_to_case_json(): ], "unit_system": {"name": "SI"}, "version": "24.2.0", + "private_attribute_asset_cache": { + "project_length_unit": "m", + "project_entity_info": { + "type_name": "GeometryEntityInfo", + "face_ids": ["face_x_1", "face_x_2", "face_x_3"], + "face_group_tag": "some_tag", + "face_attribute_names": ["some_tag"], + "grouped_faces": [ + [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "1", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_1", + ], + }, + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "3", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_3", + ], + }, + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "2", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_2", + ], + }, + ] + ], + }, + }, } params, _, _ = validate_model(param_data, "SI", "Geometry") @@ -461,6 +501,42 @@ def test_simulation_to_case_vm_workflow(): }, "unit_system": {"name": "SI"}, "version": "24.2.0", + "models": [ + { + "type": "Wall", + "entities": { + "stored_entities": [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "Mysurface", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [], + } + ] + }, + "use_wall_function": False, + } + ], + "private_attribute_asset_cache": { + "project_length_unit": None, + "project_entity_info": { + "draft_entities": [], + "type_name": "VolumeMeshEntityInfo", + "zones": [], + "boundaries": [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "Mysurface", + "private_attribute_full_name": None, + "private_attribute_is_interface": False, + "private_attribute_tag_key": None, + "private_attribute_sub_components": [], + } + ], + }, + }, } params, _, _ = validate_model(param_data, "SI", "Geometry") @@ -469,7 +545,7 @@ def test_simulation_to_case_vm_workflow(): case_json, hash = simulation_to_case_json(params, {"value": 100.0, "units": "cm"}) print(case_json) - params, _, _ = validate_model(param_data, "SI", "VolumeMesh") + params, errors, _ = validate_model(param_data, "SI", "VolumeMesh") case_json, hash = simulation_to_case_json(params, {"value": 100.0, "units": "cm"}) print(case_json) @@ -510,14 +586,50 @@ def test_simulation_to_all_translation_2(): "moment_length": {"value": 1, "units": "m"}, "area": {"value": 1, "units": "m**2"}, }, - "models": [], + "models": [ + { + "type": "Wall", + "entities": { + "stored_entities": [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "surface_x", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_1", + "face_x_2", + "face_x_3", + ], + } + ] + }, + "use_wall_function": False, + } + ], "private_attribute_asset_cache": { + "project_length_unit": "m", "project_entity_info": { "type_name": "GeometryEntityInfo", - "face_ids": ["face_x"], - "face_group_tag": "not_used", - "face_attribute_names": ["not_used"], - } + "face_ids": ["face_x_1", "face_x_2", "face_x_3"], + "face_group_tag": "some_tag", + "face_attribute_names": ["some_tag"], + "grouped_faces": [ + [ + { + "private_attribute_registry_bucket_name": "SurfaceEntityType", + "private_attribute_entity_type_name": "Surface", + "name": "surface_x", + "private_attribute_is_interface": False, + "private_attribute_sub_components": [ + "face_x_1", + "face_x_2", + "face_x_3", + ], + } + ] + ], + }, }, }