diff --git a/flow360/component/project.py b/flow360/component/project.py index 56a814a0f..07284e09b 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -12,6 +12,7 @@ import pydantic as pd import typing_extensions from flow360_schema.framework.physical_dimensions import Length +from flow360_schema.models.asset_cache import CoordinateSystemStatus, MirrorStatus from pydantic import PositiveInt from flow360.cloud.file_cache import get_shared_cloud_file_cache @@ -47,10 +48,6 @@ DraftContext, get_active_draft, ) -from flow360.component.simulation.draft_context.coordinate_system_manager import ( - CoordinateSystemStatus, -) -from flow360.component.simulation.draft_context.mirror import MirrorStatus from flow360.component.simulation.draft_context.obb.tessellation_loader import ( TessellationFileLoader, ) diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index f8d9a70a2..230de2171 100644 --- a/flow360/component/project_utils.py +++ b/flow360/component/project_utils.py @@ -5,6 +5,7 @@ from typing import Optional, Type, TypeVar, get_args from flow360_schema.framework.physical_dimensions import Length +from flow360_schema.models.asset_cache import AssetCache from pydantic import ValidationError from flow360.component.simulation import services @@ -17,7 +18,6 @@ from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.framework.entity_base import EntityList from flow360.component.simulation.framework.entity_registry import EntityRegistry -from flow360.component.simulation.framework.param_utils import AssetCache from flow360.component.simulation.outputs.outputs import ( SurfaceIntegralOutput, SurfaceOutput, diff --git a/flow360/component/simulation/draft_context/context.py b/flow360/component/simulation/draft_context/context.py index ea44019f8..54fe0869e 100644 --- a/flow360/component/simulation/draft_context/context.py +++ b/flow360/component/simulation/draft_context/context.py @@ -7,14 +7,12 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, List, Optional, Union, get_args +from flow360_schema.models.asset_cache import CoordinateSystemStatus, MirrorStatus + from flow360.component.simulation.draft_context.coordinate_system_manager import ( CoordinateSystemManager, - CoordinateSystemStatus, -) -from flow360.component.simulation.draft_context.mirror import ( - MirrorManager, - MirrorStatus, ) +from flow360.component.simulation.draft_context.mirror import MirrorManager from flow360.component.simulation.entity_info import ( DraftEntityTypes, EntityInfoModel, diff --git a/flow360/component/simulation/draft_context/coordinate_system_manager.py b/flow360/component/simulation/draft_context/coordinate_system_manager.py index 487676046..91cb882a4 100644 --- a/flow360/component/simulation/draft_context/coordinate_system_manager.py +++ b/flow360/component/simulation/draft_context/coordinate_system_manager.py @@ -3,16 +3,20 @@ from __future__ import annotations import collections -from typing import Dict, List, Literal, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import numpy as np -import pydantic as pd +from flow360_schema.models.asset_cache import ( + CoordinateSystemAssignmentGroup, + CoordinateSystemEntityRef, + CoordinateSystemParent, + CoordinateSystemStatus, +) from flow360.component.simulation.entity_operation import ( CoordinateSystem, _compose_transformation_matrices, ) -from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.framework.entity_base import EntityBase from flow360.component.simulation.framework.entity_registry import EntityRegistry from flow360.component.simulation.utils import is_exact_instance @@ -20,84 +24,6 @@ from flow360.log import log -class CoordinateSystemParent(Flow360BaseModel): - """ - Parent relationship for a coordinate system. - - This is a lightweight, serializable representation of a coordinate system hierarchy edge - used by `CoordinateSystemStatus`. - """ - - type_name: Literal["CoordinateSystemParent"] = pd.Field("CoordinateSystemParent", frozen=True) - coordinate_system_id: str - parent_id: Optional[str] = pd.Field(None) - - -class CoordinateSystemEntityRef(Flow360BaseModel): - """ - Entity reference used in assignment serialization. - - Notes - ----- - This stores an `(entity_type, entity_id)` pair instead of a direct entity instance so that - the status can be serialized and later restored against a draft's entity registry. - """ - - type_name: Literal["CoordinateSystemEntityRef"] = pd.Field( - "CoordinateSystemEntityRef", frozen=True - ) - entity_type: str - entity_id: str - - -class CoordinateSystemAssignmentGroup(Flow360BaseModel): - """ - Grouped entity assignments for a coordinate system. - - A single coordinate system can be assigned to multiple entities. This model groups the - entity references to keep the status payload compact and easy to validate. - """ - - type_name: Literal["CoordinateSystemAssignmentGroup"] = pd.Field( - "CoordinateSystemAssignmentGroup", frozen=True - ) - coordinate_system_id: str - entities: List[CoordinateSystemEntityRef] - - -class CoordinateSystemStatus(Flow360BaseModel): - """ - Serializable snapshot for front end/asset cache. - - This status is stored in an asset's private cache and restored into a `DraftContext` so - that coordinate system definitions and assignments can persist across sessions. - """ - - type_name: Literal["CoordinateSystemStatus"] = pd.Field("CoordinateSystemStatus", frozen=True) - coordinate_systems: List[CoordinateSystem] - parents: List[CoordinateSystemParent] - assignments: List[CoordinateSystemAssignmentGroup] - - @pd.model_validator(mode="after") - def _validate_unique_coordinate_system_ids_and_names(self): - """Validate that all coordinate system IDs and names are unique.""" - seen_ids = set() - seen_names = set() - for cs in self.coordinate_systems: - # Check IDs first to match the order of validation in _from_status - if cs.private_attribute_id in seen_ids: - raise ValueError( - f"[Internal] Duplicate coordinate system id '{cs.private_attribute_id}' in status." - ) - if cs.name in seen_names: - raise ValueError( - f"[Internal] Duplicate coordinate system name '{cs.name}' in status." - ) - seen_ids.add(cs.private_attribute_id) - seen_names.add(cs.name) - return self - - class CoordinateSystemManager: """ Manage coordinate systems, hierarchy, and entity assignments inside a `DraftContext`. diff --git a/flow360/component/simulation/draft_context/mirror.py b/flow360/component/simulation/draft_context/mirror.py index 1a0064edb..31b76ceb0 100644 --- a/flow360/component/simulation/draft_context/mirror.py +++ b/flow360/component/simulation/draft_context/mirror.py @@ -1,22 +1,14 @@ """Mirror plane, mirrored entities and helpers.""" -from typing import Dict, List, Literal, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union -import numpy as np -import pydantic as pd -from flow360_schema.framework.physical_dimensions import Length +from flow360_schema.models.asset_cache import MirrorStatus +from flow360_schema.models.entities import MirrorPlane -from flow360.component.simulation.entity_operation import ( - _transform_direction, - _transform_point, -) -from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.framework.entity_base import EntityBase from flow360.component.simulation.framework.entity_registry import ( EntityRegistry, EntityRegistryView, ) -from flow360.component.simulation.framework.entity_utils import generate_uuid from flow360.component.simulation.primitives import ( GeometryBodyGroup, MirroredGeometryBodyGroup, @@ -24,115 +16,9 @@ Surface, ) from flow360.component.simulation.utils import is_exact_instance -from flow360.component.types import Axis from flow360.exceptions import Flow360RuntimeError from flow360.log import log - -class MirrorPlane(EntityBase): - """ - Define a mirror plane used by `MirrorManager` to create mirrored draft entities. - - A `MirrorPlane` is a draft entity representing an infinite plane defined by a center point - and a normal direction. Mirror operations use this plane to derive mirrored entities. - - Parameters - ---------- - name : str - Mirror plane name. Must be unique within the draft. - normal : Axis - Normal direction of the mirror plane. - center : LengthType.Point - Center point of the mirror plane. - - Example - ------- - - >>> import flow360 as fl - >>> plane = fl.MirrorPlane( - ... name="MirrorPlane", - ... normal=(0, 1, 0), - ... center=(0, 0, 0) * fl.u.m, - ... ) - """ - - name: str = pd.Field() - normal: Axis = pd.Field(description="Normal direction of the plane.") - # pylint: disable=no-member - center: Length.Vector3 = pd.Field(description="Center point of the plane.") - - private_attribute_entity_type_name: Literal["MirrorPlane"] = pd.Field( - "MirrorPlane", frozen=True - ) - private_attribute_id: str = pd.Field(default_factory=generate_uuid, frozen=True) - - def _apply_transformation(self, matrix: np.ndarray) -> "MirrorPlane": - """Apply 3x4 transformation matrix, returning new transformed instance.""" - # Transform the center point - center_array = np.asarray(self.center.value) - new_center_array = _transform_point(center_array, matrix) - new_center = type(self.center)(new_center_array, self.center.units) - - # Transform and normalize the normal direction - normal_array = np.asarray(self.normal) - transformed_normal = _transform_direction(normal_array, matrix) - new_normal = tuple(transformed_normal / np.linalg.norm(transformed_normal)) - - return self.model_copy(update={"center": new_center, "normal": new_normal}) - - -# region -----------------------------Internal Model Below------------------------------------- -class MirrorStatus(Flow360BaseModel): - """ - Serializable snapshot of mirror state stored in the asset cache. - - Notes - ----- - This status stores both: - - User-authored inputs: `mirror_planes` - - Derived draft-only entities: `mirrored_geometry_body_groups` and `mirrored_surfaces` - - The derived entities are generated from mirror actions and are registered into the draft's - entity registry when a draft is created/restored. - """ - - # Note: We can do similar thing as entityList to support mirroring with EntitySelectors. - mirror_planes: List[MirrorPlane] = pd.Field(description="List of mirror planes to mirror.") - mirrored_geometry_body_groups: List[MirroredGeometryBodyGroup] = pd.Field( - description="List of mirrored geometry body groups." - ) - mirrored_surfaces: List[MirroredSurface] = pd.Field(description="List of mirrored surfaces.") - - @pd.model_validator(mode="after") - def _validate_unique_mirror_plane_names(self): - """Validate that all mirror plane names are unique.""" - seen_names = set() - for plane in self.mirror_planes: - if plane.name in seen_names: - raise ValueError( - f"Duplicate mirror plane name '{plane.name}' found in mirror status." - ) - seen_names.add(plane.name) - return self - - def is_empty(self) -> bool: - """ - Return True if no mirror planes or mirrored entities exist in this status. - - Returns - ------- - bool - True when no mirroring is configured. - """ - return ( - not self.mirror_planes - and not self.mirrored_geometry_body_groups - and not self.mirrored_surfaces - ) - - -# endregion ------------------------------------------------------------------------------------- - MIRROR_SUFFIX = "_" # region -----------------------------Internal Functions Below------------------------------------- diff --git a/flow360/component/simulation/entity_operation.py b/flow360/component/simulation/entity_operation.py index ac4c587bc..e8310a03a 100644 --- a/flow360/component/simulation/entity_operation.py +++ b/flow360/component/simulation/entity_operation.py @@ -3,7 +3,6 @@ # pylint: disable=unused-import from flow360_schema.framework.entity.entity_operation import ( # noqa: F401 CoordinateSystem, - Transformation, _build_transformation_matrix, _compose_transformation_matrices, _extract_rotation_matrix, @@ -16,3 +15,6 @@ _validate_uniform_scale_and_transform_center, rotation_matrix_from_axis_and_angle, ) +from flow360_schema.framework.entity.legacy_transformation import ( # noqa: F401 + Transformation, +) diff --git a/flow360/component/simulation/framework/boundary_split.py b/flow360/component/simulation/framework/boundary_split.py index 189953bc6..903792cd0 100644 --- a/flow360/component/simulation/framework/boundary_split.py +++ b/flow360/component/simulation/framework/boundary_split.py @@ -398,7 +398,7 @@ def update_entities_in_model( # pylint: disable=too-many-branches - Lists/tuples containing entities or models """ # pylint: disable=import-outside-toplevel - from flow360.component.simulation.framework.param_utils import AssetCache + from flow360_schema.models.asset_cache import AssetCache for field in model.__dict__.values(): if isinstance(field, AssetCache): diff --git a/flow360/component/simulation/framework/entity_materializer.py b/flow360/component/simulation/framework/entity_materializer.py index 5bb35e846..2970a8a3f 100644 --- a/flow360/component/simulation/framework/entity_materializer.py +++ b/flow360/component/simulation/framework/entity_materializer.py @@ -16,8 +16,8 @@ ) from flow360_schema.framework.entity.entity_utils import DEFAULT_NOT_MERGED_TYPES from flow360_schema.framework.validation.context import DeserializationContext +from flow360_schema.models.entities import MirrorPlane -from flow360.component.simulation.draft_context.mirror import MirrorPlane from flow360.component.simulation.outputs.output_entities import ( Point, PointArray, diff --git a/flow360/component/simulation/framework/param_utils.py b/flow360/component/simulation/framework/param_utils.py index 666a5dba0..e2f0fd221 100644 --- a/flow360/component/simulation/framework/param_utils.py +++ b/flow360/component/simulation/framework/param_utils.py @@ -2,117 +2,20 @@ # pylint: disable=no-member -from typing import List, Optional, Union +from typing import Union -import pydantic as pd -from flow360_schema.framework.expression.variable import VariableContextList -from flow360_schema.framework.physical_dimensions import Length +from flow360_schema.models.asset_cache import AssetCache -from flow360.component.simulation.draft_context.coordinate_system_manager import ( - CoordinateSystemStatus, -) -from flow360.component.simulation.draft_context.mirror import MirrorStatus -from flow360.component.simulation.entity_info import ( - GeometryEntityInfo, - SurfaceMeshEntityInfo, - VolumeMeshEntityInfo, -) from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.framework.entity_base import EntityBase, EntityList from flow360.component.simulation.framework.entity_registry import EntityRegistry -from flow360.component.simulation.framework.entity_selector import EntitySelector from flow360.component.simulation.framework.unique_list import UniqueStringList from flow360.component.simulation.primitives import ( - ImportedSurface, _SurfaceEntityBase, _VolumeEntityBase, ) -class AssetCache(Flow360BaseModel): - """ - Cached info from the project asset. - """ - - project_length_unit: Optional[Length.PositiveFloat64] = pd.Field(None, frozen=True) - project_entity_info: Optional[ - Union[GeometryEntityInfo, VolumeMeshEntityInfo, SurfaceMeshEntityInfo] - ] = pd.Field(None, frozen=True, discriminator="type_name") - use_inhouse_mesher: bool = pd.Field( - False, - description="Flag whether user requested the use of inhouse surface and volume mesher.", - ) - use_geometry_AI: bool = pd.Field( - False, description="Flag whether user requested the use of GAI." - ) - variable_context: Optional[VariableContextList] = pd.Field( - None, - description="List of user variables that are used in all the `Expression` instances.", - ) - used_selectors: Optional[List[EntitySelector]] = pd.Field( - None, - description="Collected entity selectors for token reference.", - ) - imported_surfaces: Optional[List[ImportedSurface]] = pd.Field( - None, description="List of imported surface meshes for post-processing." - ) - mirror_status: Optional[MirrorStatus] = pd.Field( - None, description="Status of mirroring operations that are used in the simulation." - ) - coordinate_system_status: Optional[CoordinateSystemStatus] = pd.Field( - None, description="Status of coordinate systems used in the simulation." - ) - - @property - def boundaries(self): - """ - Get all boundaries (not just names) from the cached entity info. - """ - if self.project_entity_info is None: - return None - return self.project_entity_info.get_boundaries() - - @pd.model_validator(mode="after") - def _validate_mirror_status_compatible_with_geometry(self): - """Raise if mirror_status has mirroring but geometry doesn't support face-to-body-group mapping.""" - if self.mirror_status is None: - return self - if not self.mirror_status.mirrored_geometry_body_groups: - return self - if not isinstance(self.project_entity_info, GeometryEntityInfo): - return self - - try: - self.project_entity_info.get_face_group_to_body_group_id_map() - except ValueError as exc: - raise ValueError( - "Mirroring is requested but the geometry's face groupings span across body groups. " - f"Mirroring cannot be performed: {exc}" - ) from exc - return self - - def preprocess( - self, - *, - params=None, - exclude: List[str] = None, - required_by: List[str] = None, - flow360_unit_system=None, - ) -> Flow360BaseModel: - # Exclude variable_context and selectors from preprocessing. - # NOTE: coordinate_system_status is NOT excluded, which means it will be - # recursively preprocessed. This is CRITICAL because CoordinateSystem objects - # contain LengthType fields (origin, translation) that must be nondimensionalized - # before transformation matrices are computed in the translator. - exclude_asset_cache = exclude + ["variable_context", "selectors"] - return super().preprocess( - params=params, - exclude=exclude_asset_cache, - required_by=required_by, - flow360_unit_system=flow360_unit_system, - ) - - def find_instances(obj, target_type): """Recursively find items of target_type within a python object""" stack = [obj] diff --git a/flow360/component/simulation/simulation_params.py b/flow360/component/simulation/simulation_params.py index c8bf9d87e..d960e53b2 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -23,6 +23,7 @@ Velocity, ) from flow360_schema.framework.validation.context import DeserializationContext +from flow360_schema.models.asset_cache import AssetCache from flow360.component.simulation.conversion import ( LIQUID_IMAGINARY_FREESTREAM_MACH, @@ -37,7 +38,6 @@ ) from flow360.component.simulation.framework.entity_registry import EntityRegistry from flow360.component.simulation.framework.param_utils import ( - AssetCache, _set_boundary_full_name_with_zone_name, _update_entity_full_name, _update_zone_boundaries_with_metadata, diff --git a/flow360/component/simulation/translator/utils.py b/flow360/component/simulation/translator/utils.py index be336c33a..034e44c1e 100644 --- a/flow360/component/simulation/translator/utils.py +++ b/flow360/component/simulation/translator/utils.py @@ -12,6 +12,7 @@ import unyt as u from flow360_schema.framework.expression import Expression, UserVariable from flow360_schema.framework.physical_dimensions import Length +from flow360_schema.models.asset_cache import AssetCache from flow360.component.simulation.draft_context.coordinate_system_manager import ( CoordinateSystemManager, @@ -19,7 +20,6 @@ from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.framework.base_model_config import snake_to_camel from flow360.component.simulation.framework.entity_base import EntityBase, EntityList -from flow360.component.simulation.framework.param_utils import AssetCache from flow360.component.simulation.framework.unique_list import UniqueItemList from flow360.component.simulation.meshing_param import snappy from flow360.component.simulation.meshing_param.params import ModularMeshingWorkflow diff --git a/poetry.lock b/poetry.lock index 57645106b..8583ddad7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1468,21 +1468,19 @@ files = [ [[package]] name = "flow360-schema" -version = "0.1.18" +version = "0.1.19" description = "Pure Pydantic schemas for Flow360 - Single Source of Truth" optional = false python-versions = ">=3.10,<4.0" groups = ["main"] files = [ - {file = "flow360_schema-0.1.18-py3-none-any.whl", hash = "sha256:739b7c9fef756c820d1e7476a84b726cd8b396b6a0178a5a2afad977c20498fa"}, - {file = "flow360_schema-0.1.18.tar.gz", hash = "sha256:d4256c3aedd6f599ab0d4970d46d3e4b882b09e4ee43e3b82977811c84add932"}, + {file = "flow360_schema-0.1.19-py3-none-any.whl", hash = "sha256:5cf3750337a3b58a672314bd3b0ab63ba8e5e43963f4a0d4b87e6976f72a52eb"}, + {file = "flow360_schema-0.1.19.tar.gz", hash = "sha256:34a40ac032555b9af317e99d877288f3564fbaea0c39340c8a6b811d64482aac"}, ] [package.dependencies] pydantic = ">=2.8,<3.0" - -[package.extras] -unyt = ["unyt (>=2.9.0)"] +unyt = ">=2.9.0" [package.source] type = "legacy" diff --git a/tests/simulation/draft_context/test_coordinate_system_assignment.py b/tests/simulation/draft_context/test_coordinate_system_assignment.py index 574501349..fbb3d9cae 100644 --- a/tests/simulation/draft_context/test_coordinate_system_assignment.py +++ b/tests/simulation/draft_context/test_coordinate_system_assignment.py @@ -281,41 +281,6 @@ def test_from_status_validation_errors(mock_geometry): ) -def test_from_status_rejects_duplicate_cs_id(mock_geometry): - """Test that CoordinateSystemStatus rejects duplicate coordinate system IDs via Pydantic validation.""" - from pydantic import ValidationError - - with create_draft(new_run_from=mock_geometry) as draft: - cs = CoordinateSystem(name="cs") - with pytest.raises( - ValidationError, - match="Duplicate coordinate system id", - ): - CoordinateSystemStatus( - coordinate_systems=[cs, cs], - parents=[], - assignments=[], - ) - - -def test_from_status_rejects_duplicate_cs_name(mock_geometry): - """Test that CoordinateSystemStatus rejects duplicate coordinate system names via Pydantic validation.""" - from pydantic import ValidationError - - with create_draft(new_run_from=mock_geometry) as draft: - cs1 = CoordinateSystem(name="duplicate") - cs2 = CoordinateSystem(name="duplicate") - with pytest.raises( - ValidationError, - match="Duplicate coordinate system name 'duplicate'", - ): - CoordinateSystemStatus( - coordinate_systems=[cs1, cs2], - parents=[], - assignments=[], - ) - - def test_from_status_rejects_assignment_unknown_cs(mock_geometry): with create_draft(new_run_from=mock_geometry) as draft: status = CoordinateSystemStatus( diff --git a/tests/simulation/draft_context/test_entity_transformation.py b/tests/simulation/draft_context/test_entity_transformation.py deleted file mode 100644 index 4128b6cc9..000000000 --- a/tests/simulation/draft_context/test_entity_transformation.py +++ /dev/null @@ -1,459 +0,0 @@ -"""Tests for entity transformation methods (_apply_transformation). - -This test module verifies that all entities with coordinate system support -correctly apply 3x4 transformation matrices (rotation + translation + scale). -""" - -import numpy as np -import pytest - -import flow360.component.simulation.units as u -from flow360.component.simulation.draft_context.mirror import MirrorPlane -from flow360.component.simulation.entity_operation import CoordinateSystem -from flow360.component.simulation.outputs.output_entities import ( - Point, - PointArray, - PointArray2D, - Slice, -) -from flow360.component.simulation.primitives import Box, Cylinder, Sphere -from flow360.component.simulation.unit_system import SI_unit_system -from flow360.exceptions import Flow360ValueError - - -# Simple transformation matrices for testing -def identity_matrix(): - """Identity transformation (no change).""" - return np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]], dtype=np.float64) - - -def translation_matrix(tx, ty, tz): - """Pure translation.""" - return np.array([[1, 0, 0, tx], [0, 1, 0, ty], [0, 0, 1, tz]], dtype=np.float64) - - -def rotation_z_90(): - """90 degree rotation around Z axis.""" - return np.array([[0, -1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0]], dtype=np.float64) - - -def uniform_scale_matrix(scale, tx=0, ty=0, tz=0): - """Uniform scaling with optional translation.""" - return np.array([[scale, 0, 0, tx], [0, scale, 0, ty], [0, 0, scale, tz]], dtype=np.float64) - - -def non_uniform_scale_matrix(): - """Non-uniform scaling (different scale on each axis).""" - return np.array([[2, 0, 0, 0], [0, 3, 0, 0], [0, 0, 4, 0]], dtype=np.float64) - - -# ============================================================================== -# Point Tests -# ============================================================================== - - -def test_point_identity_transformation(): - """Point with identity matrix should remain unchanged.""" - with SI_unit_system: - point = Point(name="test_point", location=(1, 2, 3) * u.m) - transformed = point._apply_transformation(identity_matrix()) - - assert transformed.name == "test_point" - np.testing.assert_allclose(transformed.location.value, [1, 2, 3], atol=1e-10) - - -def test_point_translation(): - """Point should translate correctly.""" - with SI_unit_system: - point = Point(name="test_point", location=(1, 2, 3) * u.m) - matrix = translation_matrix(10, 20, 30) - transformed = point._apply_transformation(matrix) - - # Expected: (1, 2, 3) + (10, 20, 30) = (11, 22, 33) - np.testing.assert_allclose(transformed.location.value, [11, 22, 33], atol=1e-10) - - -def test_point_rotation(): - """Point should rotate correctly.""" - with SI_unit_system: - point = Point(name="test_point", location=(1, 0, 0) * u.m) - matrix = rotation_z_90() - transformed = point._apply_transformation(matrix) - - # Expected: 90° rotation of (1, 0, 0) around Z = (0, 1, 0) - np.testing.assert_allclose(transformed.location.value, [0, 1, 0], atol=1e-10) - - -def test_point_uniform_scale(): - """Point should scale correctly.""" - with SI_unit_system: - point = Point(name="test_point", location=(1, 2, 3) * u.m) - matrix = uniform_scale_matrix(2.0) - transformed = point._apply_transformation(matrix) - - # Expected: (1, 2, 3) * 2 = (2, 4, 6) - np.testing.assert_allclose(transformed.location.value, [2, 4, 6], atol=1e-10) - - -# ============================================================================== -# PointArray Tests -# ============================================================================== - - -def test_point_array_translation(): - """PointArray should translate both start and end points.""" - with SI_unit_system: - point_array = PointArray( - name="test_array", start=(0, 0, 0) * u.m, end=(10, 0, 0) * u.m, number_of_points=5 - ) - matrix = translation_matrix(5, 10, 15) - transformed = point_array._apply_transformation(matrix) - - # Expected: start (0,0,0) + (5,10,15) = (5,10,15) - # end (10,0,0) + (5,10,15) = (15,10,15) - np.testing.assert_allclose(transformed.start.value, [5, 10, 15], atol=1e-10) - np.testing.assert_allclose(transformed.end.value, [15, 10, 15], atol=1e-10) - assert transformed.number_of_points == 5 - - -def test_point_array_rotation(): - """PointArray should rotate correctly.""" - with SI_unit_system: - point_array = PointArray( - name="test_array", start=(1, 0, 0) * u.m, end=(2, 0, 0) * u.m, number_of_points=3 - ) - matrix = rotation_z_90() - transformed = point_array._apply_transformation(matrix) - - # Expected: 90° rotation around Z - # start (1,0,0) -> (0,1,0) - # end (2,0,0) -> (0,2,0) - np.testing.assert_allclose(transformed.start.value, [0, 1, 0], atol=1e-10) - np.testing.assert_allclose(transformed.end.value, [0, 2, 0], atol=1e-10) - - -# ============================================================================== -# PointArray2D Tests -# ============================================================================== - - -def test_point_array_2d_translation(): - """PointArray2D should translate origin.""" - with SI_unit_system: - array_2d = PointArray2D( - name="test_2d", - origin=(0, 0, 0) * u.m, - u_axis_vector=(1, 0, 0) * u.m, - v_axis_vector=(0, 1, 0) * u.m, - u_number_of_points=3, - v_number_of_points=3, - ) - matrix = translation_matrix(10, 20, 30) - transformed = array_2d._apply_transformation(matrix) - - np.testing.assert_allclose(transformed.origin.value, [10, 20, 30], atol=1e-10) - # Axis vectors should remain unchanged (pure translation) - np.testing.assert_allclose(transformed.u_axis_vector.value, [1, 0, 0], atol=1e-10) - np.testing.assert_allclose(transformed.v_axis_vector.value, [0, 1, 0], atol=1e-10) - - -def test_point_array_2d_rotation(): - """PointArray2D should rotate origin and axis vectors.""" - with SI_unit_system: - array_2d = PointArray2D( - name="test_2d", - origin=(1, 0, 0) * u.m, - u_axis_vector=(1, 0, 0) * u.m, - v_axis_vector=(0, 1, 0) * u.m, - u_number_of_points=2, - v_number_of_points=2, - ) - matrix = rotation_z_90() - transformed = array_2d._apply_transformation(matrix) - - # Origin (1,0,0) rotated 90° around Z = (0,1,0) - np.testing.assert_allclose(transformed.origin.value, [0, 1, 0], atol=1e-10) - # u_axis (1,0,0) rotated = (0,1,0) - np.testing.assert_allclose(transformed.u_axis_vector.value, [0, 1, 0], atol=1e-10) - # v_axis (0,1,0) rotated = (-1,0,0) - np.testing.assert_allclose(transformed.v_axis_vector.value, [-1, 0, 0], atol=1e-10) - - -# ============================================================================== -# Slice Tests -# ============================================================================== - - -def test_slice_translation(): - """Slice should translate origin.""" - with SI_unit_system: - slice_obj = Slice(name="test_slice", origin=(0, 0, 0) * u.m, normal=(0, 0, 1)) - matrix = translation_matrix(5, 10, 15) - transformed = slice_obj._apply_transformation(matrix) - - np.testing.assert_allclose(transformed.origin.value, [5, 10, 15], atol=1e-10) - # Normal should remain unchanged (pure translation) - np.testing.assert_allclose(transformed.normal, [0, 0, 1], atol=1e-10) - - -def test_slice_rotation(): - """Slice should rotate normal vector.""" - with SI_unit_system: - slice_obj = Slice(name="test_slice", origin=(0, 0, 0) * u.m, normal=(1, 0, 0)) - matrix = rotation_z_90() - transformed = slice_obj._apply_transformation(matrix) - - # Origin unchanged - np.testing.assert_allclose(transformed.origin.value, [0, 0, 0], atol=1e-10) - # Normal (1,0,0) rotated 90° around Z = (0,1,0) - np.testing.assert_allclose(transformed.normal, [0, 1, 0], atol=1e-10) - - -# ============================================================================== -# MirrorPlane Tests -# ============================================================================== - - -def test_mirror_plane_translation(): - """MirrorPlane should translate center.""" - with SI_unit_system: - mirror = MirrorPlane(name="test_mirror", center=(0, 0, 0) * u.m, normal=(0, 1, 0)) - matrix = translation_matrix(10, 20, 30) - transformed = mirror._apply_transformation(matrix) - - np.testing.assert_allclose(transformed.center.value, [10, 20, 30], atol=1e-10) - np.testing.assert_allclose(transformed.normal, [0, 1, 0], atol=1e-10) - - -def test_mirror_plane_rotation(): - """MirrorPlane should rotate normal vector.""" - with SI_unit_system: - mirror = MirrorPlane(name="test_mirror", center=(1, 0, 0) * u.m, normal=(1, 0, 0)) - matrix = rotation_z_90() - transformed = mirror._apply_transformation(matrix) - - # Center (1,0,0) rotated = (0,1,0) - np.testing.assert_allclose(transformed.center.value, [0, 1, 0], atol=1e-10) - # Normal (1,0,0) rotated = (0,1,0) - np.testing.assert_allclose(transformed.normal, [0, 1, 0], atol=1e-10) - - -# ============================================================================== -# Box Tests (with uniform scaling validation) -# ============================================================================== - - -def test_box_identity(): - """Box with identity matrix should remain unchanged.""" - with SI_unit_system: - box = Box(name="test_box", center=(0, 0, 0) * u.m, size=(2, 2, 2) * u.m) - transformed = box._apply_transformation(identity_matrix()) - - np.testing.assert_allclose(transformed.center.value, [0, 0, 0], atol=1e-10) - np.testing.assert_allclose(transformed.size.value, [2, 2, 2], atol=1e-10) - - -def test_box_translation(): - """Box should translate center.""" - with SI_unit_system: - box = Box(name="test_box", center=(1, 2, 3) * u.m, size=(2, 4, 6) * u.m) - matrix = translation_matrix(10, 20, 30) - transformed = box._apply_transformation(matrix) - - np.testing.assert_allclose(transformed.center.value, [11, 22, 33], atol=1e-10) - # Size unchanged by translation - np.testing.assert_allclose(transformed.size.value, [2, 4, 6], atol=1e-10) - - -def test_box_uniform_scale(): - """Box should scale size uniformly.""" - with SI_unit_system: - box = Box(name="test_box", center=(1, 0, 0) * u.m, size=(2, 4, 6) * u.m) - matrix = uniform_scale_matrix(2.0) - transformed = box._apply_transformation(matrix) - - # Center scaled: (1,0,0) * 2 = (2,0,0) - np.testing.assert_allclose(transformed.center.value, [2, 0, 0], atol=1e-10) - # Size scaled: (2,4,6) * 2 = (4,8,12) - np.testing.assert_allclose(transformed.size.value, [4, 8, 12], atol=1e-10) - - -def test_box_rotation(): - """Box should rotate axis_of_rotation.""" - with SI_unit_system: - box = Box( - name="test_box", - center=(0, 0, 0) * u.m, - size=(2, 2, 2) * u.m, - axis_of_rotation=(1, 0, 0), - angle_of_rotation=45 * u.deg, - ) - matrix = rotation_z_90() - transformed = box._apply_transformation(matrix) - - # Axis (1,0,0) rotated 90° around Z = (0,1,0) - # The combined rotation should be applied - # This is a complex test - just verify it doesn't crash - assert transformed.center.value is not None - - -def test_box_non_uniform_scale_raises_error(): - """Box should reject non-uniform scaling.""" - with SI_unit_system: - box = Box(name="test_box", center=(0, 0, 0) * u.m, size=(2, 2, 2) * u.m) - matrix = non_uniform_scale_matrix() - - with pytest.raises(Flow360ValueError, match="only supports uniform scaling"): - box._apply_transformation(matrix) - - -# ============================================================================== -# Sphere Tests (with uniform scaling validation) -# ============================================================================== - - -def test_sphere_identity(): - """Sphere with identity matrix should remain unchanged.""" - with SI_unit_system: - sphere = Sphere(name="test_sphere", center=(0, 0, 0) * u.m, radius=5 * u.m) - transformed = sphere._apply_transformation(identity_matrix()) - - np.testing.assert_allclose(transformed.center.value, [0, 0, 0], atol=1e-10) - np.testing.assert_allclose(transformed.radius.value, 5, atol=1e-10) - - -def test_sphere_translation(): - """Sphere should translate center.""" - with SI_unit_system: - sphere = Sphere(name="test_sphere", center=(1, 2, 3) * u.m, radius=5 * u.m) - matrix = translation_matrix(10, 20, 30) - transformed = sphere._apply_transformation(matrix) - - np.testing.assert_allclose(transformed.center.value, [11, 22, 33], atol=1e-10) - # Radius unchanged by translation - np.testing.assert_allclose(transformed.radius.value, 5, atol=1e-10) - - -def test_sphere_uniform_scale(): - """Sphere should scale radius uniformly.""" - with SI_unit_system: - sphere = Sphere(name="test_sphere", center=(1, 0, 0) * u.m, radius=5 * u.m) - matrix = uniform_scale_matrix(2.0) - transformed = sphere._apply_transformation(matrix) - - # Center scaled: (1,0,0) * 2 = (2,0,0) - np.testing.assert_allclose(transformed.center.value, [2, 0, 0], atol=1e-10) - # Radius scaled: 5 * 2 = 10 - np.testing.assert_allclose(transformed.radius.value, 10, atol=1e-10) - - -def test_sphere_rotation(): - """Sphere center and axis should rotate (radius unchanged).""" - with SI_unit_system: - sphere = Sphere(name="test_sphere", center=(1, 0, 0) * u.m, radius=5 * u.m, axis=(1, 0, 0)) - matrix = rotation_z_90() - transformed = sphere._apply_transformation(matrix) - - # Center (1,0,0) rotated 90° around Z = (0,1,0) - np.testing.assert_allclose(transformed.center.value, [0, 1, 0], atol=1e-10) - # Axis (1,0,0) rotated 90° around Z = (0,1,0) - np.testing.assert_allclose(transformed.axis, (0, 1, 0), atol=1e-10) - # Radius unchanged by rotation - np.testing.assert_allclose(transformed.radius.value, 5, atol=1e-10) - - -def test_sphere_non_uniform_scale_raises_error(): - """Sphere should reject non-uniform scaling.""" - with SI_unit_system: - sphere = Sphere(name="test_sphere", center=(0, 0, 0) * u.m, radius=5 * u.m) - matrix = non_uniform_scale_matrix() - - with pytest.raises(Flow360ValueError, match="only supports uniform scaling"): - sphere._apply_transformation(matrix) - - -# ============================================================================== -# Cylinder Tests (with uniform scaling validation) -# ============================================================================== - - -def test_cylinder_translation(): - """Cylinder should translate center.""" - with SI_unit_system: - cylinder = Cylinder( - name="test_cyl", - center=(0, 0, 0) * u.m, - axis=(0, 0, 1), - height=10 * u.m, - outer_radius=2 * u.m, - ) - matrix = translation_matrix(5, 10, 15) - transformed = cylinder._apply_transformation(matrix) - - np.testing.assert_allclose(transformed.center.value, [5, 10, 15], atol=1e-10) - # Axis unchanged - np.testing.assert_allclose(transformed.axis, [0, 0, 1], atol=1e-10) - - -def test_cylinder_rotation(): - """Cylinder should rotate axis.""" - with SI_unit_system: - cylinder = Cylinder( - name="test_cyl", - center=(0, 0, 0) * u.m, - axis=(1, 0, 0), - height=10 * u.m, - outer_radius=2 * u.m, - ) - matrix = rotation_z_90() - transformed = cylinder._apply_transformation(matrix) - - # Axis (1,0,0) rotated 90° around Z = (0,1,0) - np.testing.assert_allclose(transformed.axis, [0, 1, 0], atol=1e-10) - - -def test_cylinder_uniform_scale(): - """Cylinder should scale uniformly.""" - with SI_unit_system: - cylinder = Cylinder( - name="test_cyl", - center=(1, 0, 0) * u.m, - axis=(0, 0, 1), - height=10 * u.m, - outer_radius=2 * u.m, - inner_radius=1 * u.m, - ) - matrix = uniform_scale_matrix(2.0) - transformed = cylinder._apply_transformation(matrix) - - # Center scaled - np.testing.assert_allclose(transformed.center.value, [2, 0, 0], atol=1e-10) - # Dimensions scaled - np.testing.assert_allclose(transformed.height.value, 20, atol=1e-10) - np.testing.assert_allclose(transformed.outer_radius.value, 4, atol=1e-10) - np.testing.assert_allclose(transformed.inner_radius.value, 2, atol=1e-10) - - -def test_cylinder_non_uniform_scale_raises_error(): - """Cylinder should reject non-uniform scaling.""" - with SI_unit_system: - cylinder = Cylinder( - name="test_cyl", - center=(0, 0, 0) * u.m, - axis=(0, 0, 1), - height=10 * u.m, - outer_radius=2 * u.m, - ) - matrix = non_uniform_scale_matrix() - - with pytest.raises(Flow360ValueError, match="only supports uniform scaling"): - cylinder._apply_transformation(matrix) - - -# ============================================================================== -# AxisymmetricBody Tests (with uniform scaling validation) -# ============================================================================== - -# Note: AxisymmetricBody and CustomVolume tests skipped for now -# They require complex construction with specific boundary conditions -# The transformation logic has been implemented and tested via other entities diff --git a/tests/simulation/draft_context/test_mirror_action.py b/tests/simulation/draft_context/test_mirror_action.py index 3838b883d..e50475e5f 100644 --- a/tests/simulation/draft_context/test_mirror_action.py +++ b/tests/simulation/draft_context/test_mirror_action.py @@ -1,6 +1,5 @@ import json import os -from unittest.mock import MagicMock import pytest from pydantic import ValidationError @@ -17,12 +16,7 @@ MirrorStatus, _derive_mirrored_entities_from_actions, ) -from flow360.component.simulation.entity_info import GeometryEntityInfo -from flow360.component.simulation.framework.param_utils import AssetCache -from flow360.component.simulation.primitives import ( - GeometryBodyGroup, - MirroredGeometryBodyGroup, -) +from flow360.component.simulation.primitives import GeometryBodyGroup from flow360.component.simulation.simulation_params import SimulationParams from flow360.exceptions import Flow360RuntimeError @@ -289,33 +283,6 @@ def test_mirror_create_rejects_duplicate_plane_name(mock_geometry): draft.mirror.create_mirror_of(entities=body_groups[0], mirror_plane=mirror_plane2) -def test_mirror_from_status_rejects_duplicate_plane_names(mock_geometry): - """Test that MirrorStatus rejects duplicate mirror plane names via Pydantic validation.""" - with create_draft(new_run_from=mock_geometry) as draft: - body_groups = list(draft.body_groups) - assert body_groups, "Test requires at least one body group." - - # Create a status with duplicate mirror plane names. - plane1 = MirrorPlane(name="duplicate", normal=(1, 0, 0), center=(0, 0, 0) * u.m) - plane2 = MirrorPlane(name="duplicate", normal=(0, 1, 0), center=(0, 0, 0) * u.m) - - # Build a MirrorStatus with duplicate plane names - should fail during construction. - from flow360.component.simulation.primitives import MirroredGeometryBodyGroup - - with pytest.raises(ValidationError, match="Duplicate mirror plane name 'duplicate'"): - MirrorStatus( - mirror_planes=[plane1, plane2], - mirrored_geometry_body_groups=[ - MirroredGeometryBodyGroup( - name=f"{body_groups[0].name}_", - geometry_body_group_id=body_groups[0].private_attribute_id, - mirror_plane_id=plane1.private_attribute_id, - ) - ], - mirrored_surfaces=[], - ) - - def test_remove_mirror_of_removes_mirror_assignment(mock_geometry): """Test that remove_mirror_of successfully removes mirror assignments.""" with create_draft(new_run_from=mock_geometry) as draft: @@ -573,171 +540,6 @@ def test_mirror_create_raises_when_face_group_to_body_group_is_none(mock_geometr mirror_manager.create_mirror_of(entities=body_group, mirror_plane=mirror_plane) -def test_asset_cache_validator_raises_when_mirror_status_conflicts_with_geometry(): - """Test that AssetCache validator raises when mirror_status has mirrorings but geometry doesn't support it.""" - # Create a mock GeometryEntityInfo that raises ValueError when get_face_group_to_body_group_id_map() is called - mock_entity_info = MagicMock(spec=GeometryEntityInfo) - mock_entity_info.get_face_group_to_body_group_id_map.side_effect = ValueError( - "Face group 'test_face' contains faces belonging to multiple body groups" - ) - - # Create a MirrorStatus with meaningful mirrorings - mirror_plane = MirrorPlane( - name="mirrorX", - normal=(1, 0, 0), - center=(0, 0, 0) * u.m, - ) - mirrored_body_group = MirroredGeometryBodyGroup( - name="test_body_group_", - geometry_body_group_id="test-body-group-id", - mirror_plane_id=mirror_plane.private_attribute_id, - ) - mirror_status = MirrorStatus( - mirror_planes=[mirror_plane], - mirrored_geometry_body_groups=[mirrored_body_group], - mirrored_surfaces=[], - ) - - # Use model_construct to bypass field validation and directly set values - # Then call the validator method directly - asset_cache = AssetCache.model_construct( - project_entity_info=mock_entity_info, - mirror_status=mirror_status, - ) - - # Call the validator directly - it should raise ValueError - with pytest.raises( - ValueError, - match="Mirroring is requested but the geometry's face groupings span across body groups", - ): - asset_cache._validate_mirror_status_compatible_with_geometry() - - -def test_asset_cache_validator_passes_when_no_mirror_status(): - """Test that AssetCache validator passes when mirror_status is None.""" - # Create a mock GeometryEntityInfo that would raise if called - mock_entity_info = MagicMock(spec=GeometryEntityInfo) - mock_entity_info.get_face_group_to_body_group_id_map.side_effect = ValueError( - "This should not be called" - ) - - # Use model_construct to bypass field validation - asset_cache = AssetCache.model_construct( - project_entity_info=mock_entity_info, - mirror_status=None, - ) - - # Validator should pass and return self - result = asset_cache._validate_mirror_status_compatible_with_geometry() - assert result is asset_cache - # Verify the method was never called - mock_entity_info.get_face_group_to_body_group_id_map.assert_not_called() - - -def test_asset_cache_validator_passes_when_empty_mirrored_body_groups(): - """Test that AssetCache validator passes when mirror_status has no mirrored body groups.""" - mock_entity_info = MagicMock(spec=GeometryEntityInfo) - mock_entity_info.get_face_group_to_body_group_id_map.side_effect = ValueError( - "This should not be called" - ) - - # MirrorStatus with empty mirrored_geometry_body_groups should not trigger validation - mirror_status = MirrorStatus( - mirror_planes=[], - mirrored_geometry_body_groups=[], - mirrored_surfaces=[], - ) - - # Use model_construct to bypass field validation - asset_cache = AssetCache.model_construct( - project_entity_info=mock_entity_info, - mirror_status=mirror_status, - ) - - # Validator should pass and return self - result = asset_cache._validate_mirror_status_compatible_with_geometry() - assert result is asset_cache - # Verify the method was never called - mock_entity_info.get_face_group_to_body_group_id_map.assert_not_called() - - -def test_asset_cache_validator_passes_when_geometry_supports_mirroring(): - """Test that AssetCache validator passes when geometry supports mirroring.""" - # Create a mock GeometryEntityInfo that returns valid mapping - mock_entity_info = MagicMock(spec=GeometryEntityInfo) - mock_entity_info.get_face_group_to_body_group_id_map.return_value = { - "surface1": "body_group_1", - "surface2": "body_group_1", - } - - # Create a MirrorStatus with meaningful mirrorings - mirror_plane = MirrorPlane( - name="mirrorX", - normal=(1, 0, 0), - center=(0, 0, 0) * u.m, - ) - mirrored_body_group = MirroredGeometryBodyGroup( - name="test_body_group_", - geometry_body_group_id="test-body-group-id", - mirror_plane_id=mirror_plane.private_attribute_id, - ) - mirror_status = MirrorStatus( - mirror_planes=[mirror_plane], - mirrored_geometry_body_groups=[mirrored_body_group], - mirrored_surfaces=[], - ) - - # Use model_construct to bypass field validation - asset_cache = AssetCache.model_construct( - project_entity_info=mock_entity_info, - mirror_status=mirror_status, - ) - - # Validator should pass and return self - result = asset_cache._validate_mirror_status_compatible_with_geometry() - assert result is asset_cache - # Verify the method was called - mock_entity_info.get_face_group_to_body_group_id_map.assert_called_once() - - -def test_asset_cache_validator_skips_non_geometry_entity_info(): - """Test that AssetCache validator skips validation when entity_info is not GeometryEntityInfo.""" - # Use a mock that doesn't have isinstance(x, GeometryEntityInfo) returning True - mock_entity_info = MagicMock() # Not spec=GeometryEntityInfo - mock_entity_info.get_face_group_to_body_group_id_map.side_effect = ValueError( - "This should not be called" - ) - - # Create a MirrorStatus with meaningful mirrorings - mirror_plane = MirrorPlane( - name="mirrorX", - normal=(1, 0, 0), - center=(0, 0, 0) * u.m, - ) - mirrored_body_group = MirroredGeometryBodyGroup( - name="test_body_group_", - geometry_body_group_id="test-body-group-id", - mirror_plane_id=mirror_plane.private_attribute_id, - ) - mirror_status = MirrorStatus( - mirror_planes=[mirror_plane], - mirrored_geometry_body_groups=[mirrored_body_group], - mirrored_surfaces=[], - ) - - # Use model_construct to bypass field validation - asset_cache = AssetCache.model_construct( - project_entity_info=mock_entity_info, - mirror_status=mirror_status, - ) - - # Validator should pass because entity_info is not GeometryEntityInfo - result = asset_cache._validate_mirror_status_compatible_with_geometry() - assert result is asset_cache - # Verify the method was never called because isinstance check should fail - mock_entity_info.get_face_group_to_body_group_id_map.assert_not_called() - - # -------------------------------------------------------------------------------------- # Tests for mirror status when face groupings changes # -------------------------------------------------------------------------------------- diff --git a/tests/simulation/framework/test_pre_deserialized_entity_info.py b/tests/simulation/framework/test_pre_deserialized_entity_info.py index fe205a1d6..97725d305 100644 --- a/tests/simulation/framework/test_pre_deserialized_entity_info.py +++ b/tests/simulation/framework/test_pre_deserialized_entity_info.py @@ -1,9 +1,8 @@ -"""Tests for pre-deserialized entity_info optimization in validate_model().""" +"""Tests for validate_model() dict substitution optimization.""" import pytest from flow360.component.simulation.entity_info import VolumeMeshEntityInfo -from flow360.component.simulation.framework.param_utils import AssetCache from flow360.component.simulation.primitives import Surface @@ -17,66 +16,8 @@ def volume_mesh_entity_info(): ) -@pytest.fixture -def volume_mesh_entity_info_dict(volume_mesh_entity_info): - """Sample VolumeMeshEntityInfo as dict.""" - return volume_mesh_entity_info.model_dump(mode="json") - - -class TestAssetCacheWithPreDeserializedEntityInfo: - """Tests for AssetCache accepting pre-deserialized entity_info directly.""" - - def test_asset_cache_accepts_entity_info_dict(self, volume_mesh_entity_info_dict): - """AssetCache correctly deserializes entity_info from dict.""" - asset_cache = AssetCache(project_entity_info=volume_mesh_entity_info_dict) - - assert asset_cache.project_entity_info is not None - assert isinstance(asset_cache.project_entity_info, VolumeMeshEntityInfo) - assert len(asset_cache.project_entity_info.boundaries) == 1 - assert asset_cache.project_entity_info.boundaries[0].name == "wall" - - def test_asset_cache_accepts_entity_info_object_directly(self, volume_mesh_entity_info): - """AssetCache accepts pre-deserialized entity_info object directly.""" - # This is the key optimization: pass the object directly instead of dict - asset_cache = AssetCache(project_entity_info=volume_mesh_entity_info) - - # Should be the exact same object (identity preserved) - assert asset_cache.project_entity_info is volume_mesh_entity_info - - def test_object_identity_preserved_with_marker(self, volume_mesh_entity_info): - """Verify object identity is preserved by checking a marker attribute.""" - # Add a marker attribute to verify identity - object.__setattr__(volume_mesh_entity_info, "_test_marker", "unique_marker_12345") - - asset_cache = AssetCache(project_entity_info=volume_mesh_entity_info) - - # The marker should still be present (same object) - assert hasattr(asset_cache.project_entity_info, "_test_marker") - assert asset_cache.project_entity_info._test_marker == "unique_marker_12345" - - def test_none_entity_info_stays_none(self): - """None entity_info should remain None.""" - asset_cache = AssetCache(project_entity_info=None) - assert asset_cache.project_entity_info is None - - def test_full_asset_cache_with_pre_deserialized(self, volume_mesh_entity_info): - """Full AssetCache with multiple fields and pre-deserialized entity_info.""" - asset_cache = AssetCache( - project_length_unit={"value": 1.0, "units": "m"}, - project_entity_info=volume_mesh_entity_info, - use_inhouse_mesher=True, - use_geometry_AI=False, - ) - - # Verify all fields are correct - assert asset_cache.project_entity_info is volume_mesh_entity_info - assert asset_cache.use_inhouse_mesher is True - assert asset_cache.use_geometry_AI is False - assert asset_cache.project_length_unit is not None - - -class TestDictSubstitutionOptimization: - """Tests verifying the dict substitution approach works correctly.""" +class TestValidateModelDictSubstitutionOptimization: + """Tests verifying validate_model() dict substitution works correctly.""" def test_shallow_copy_does_not_affect_original(self, volume_mesh_entity_info): """Shallow copy of dict with substituted entity_info doesn't affect original.""" @@ -110,21 +51,3 @@ def test_shallow_copy_does_not_affect_original(self, volume_mesh_entity_info): ) # Other fields are shared (shallow copy) assert new_dict["other_field"] is original_dict["other_field"] - - def test_different_deserializations_create_distinct_objects(self, volume_mesh_entity_info_dict): - """Without optimization, each deserialization creates distinct objects.""" - asset_cache1 = AssetCache(project_entity_info=volume_mesh_entity_info_dict) - asset_cache2 = AssetCache(project_entity_info=volume_mesh_entity_info_dict) - - # Without optimization, each call creates a new entity_info object - assert asset_cache1.project_entity_info is not asset_cache2.project_entity_info - - def test_same_object_reused_when_passed_directly(self, volume_mesh_entity_info): - """When same object is passed directly, identity is preserved.""" - asset_cache1 = AssetCache(project_entity_info=volume_mesh_entity_info) - asset_cache2 = AssetCache(project_entity_info=volume_mesh_entity_info) - - # Both use the same object - assert asset_cache1.project_entity_info is volume_mesh_entity_info - assert asset_cache2.project_entity_info is volume_mesh_entity_info - assert asset_cache1.project_entity_info is asset_cache2.project_entity_info