Source code for flow360.component.simulation.draft_context.context

"""Draft context manager for local entity sandboxing."""

from __future__ import annotations

from contextlib import AbstractContextManager
from contextvars import ContextVar, Token
from dataclasses import dataclass
from typing import List, Optional, get_args

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.entity_info import (
    DraftEntityTypes,
    EntityInfoModel,
    GeometryEntityInfo,
)
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_selector import EntitySelector
from flow360.component.simulation.primitives import (
    Edge,
    GenericVolume,
    GeometryBodyGroup,
    ImportedSurface,
    MirroredGeometryBodyGroup,
    MirroredSurface,
    Surface,
)
from flow360.exceptions import Flow360RuntimeError, Flow360ValueError
from flow360.log import log

__all__ = [
    "DraftContext",
    "get_active_draft",
]


_ACTIVE_DRAFT: ContextVar[Optional["DraftContext"]] = ContextVar("_ACTIVE_DRAFT", default=None)

_DRAFT_ENTITY_TYPE_TUPLE: tuple[type[EntityBase], ...] = tuple(
    get_args(get_args(DraftEntityTypes)[0])
)


def get_active_draft() -> Optional["DraftContext"]:
    """Return the current active draft context if any."""
    return _ACTIVE_DRAFT.get()


[docs] class DraftContext( # pylint: disable=too-many-instance-attributes AbstractContextManager["DraftContext"] ): """ Context manager that tracks locally modified simulation entities/status. This should (eventually, not right now) be replacement of accessing entities directly from assets. """ __slots__ = ( # Persistent entities data storage. "_entity_info", # Interface accessing ALL types of entities. "_entity_registry", "_imported_surfaces", "_imported_geometries", # Lightweight mirror relationships storage (compared to entity storages) "_mirror_manager", # Internal mirror related entities data storage. "_mirror_status", # Lightweight coordinate system relationships storage (compared to entity storages) "_coordinate_system_manager", "_token", ) # pylint: disable=too-many-arguments def __init__( self, *, entity_info: EntityInfoModel, imported_geometries: Optional[List] = None, imported_surfaces: Optional[List[ImportedSurface]] = None, mirror_status: Optional[MirrorStatus] = None, coordinate_system_status: Optional[CoordinateSystemStatus] = None, ) -> None: """ Data members: - _token: Token to track the active draft context. - _mirror_manager: Manager for mirror planes and mirrored entities. - _entity_registry: Registry of entities of self._entity_info. This provides interface for user to access the entities in the draft. """ if entity_info is None: raise Flow360RuntimeError( "[Internal] DraftContext requires `entity_info` to initialize." ) self._token: Optional[Token] = None # DraftContext owns a deep copy of entity_info and mirror_status (created by create_draft()). # This signals transfer of entity ownership from the asset to the draft (context). self._entity_info = entity_info # Use EntityRegistry.from_entity_info() for the new DraftContext workflow. # This builds the registry by referencing entities from our copied entity_info. self._entity_registry: EntityRegistry = EntityRegistry.from_entity_info(entity_info) self._imported_surfaces: List = imported_surfaces or [] known_frozen_hashes = set() for imported_surface in self._imported_surfaces: known_frozen_hashes = self._entity_registry.fast_register( imported_surface, known_frozen_hashes ) self._imported_geometries: List = imported_geometries if imported_geometries else [] # Pre-compute face_group_to_body_group map for mirror operations. # This is only available for GeometryEntityInfo. face_group_to_body_group = None if isinstance(self._entity_info, GeometryEntityInfo): try: face_group_to_body_group = self._entity_info.get_face_group_to_body_group_id_map() except Flow360ValueError as exc: # Face grouping spans across body groups. log.warning( "Failed to derive surface-to-body-group mapping for mirroring: %s. " "Mirroring will be disabled.", exc, ) self._mirror_manager = MirrorManager._from_status( status=mirror_status, face_group_to_body_group=face_group_to_body_group, entity_registry=self._entity_registry, ) self._coordinate_system_manager = CoordinateSystemManager._from_status( status=coordinate_system_status, entity_registry=self._entity_registry, ) def __enter__(self) -> DraftContext: if get_active_draft() is not None: raise Flow360RuntimeError("Nested draft contexts are not allowed.") self._token = _ACTIVE_DRAFT.set(self) return self def __exit__(self, exc_type, exc, exc_tb) -> None: if self._token is None: raise Flow360RuntimeError( "[Internal] DraftContext exit called without a matching enter." ) _ACTIVE_DRAFT.reset(self._token) self._token = None return False # region -----------------------------Private implementations Below----------------------------- # endregion ------------------------------------------------------------------------------------ # region -----------------------------Public properties Below------------------------------------- # Persistent entities @property def body_groups(self) -> EntityRegistryView: """ Return the list of body groups in the draft. Example ------- >>> with fl.create_draft(new_run_from=geometry) as draft: ... draft.body_groups["body_group_1"] ... draft.body_groups["body_group*"] """ return self._entity_registry.view(GeometryBodyGroup) @property def surfaces(self) -> EntityRegistryView: """ Return the list of surfaces in the draft. """ return self._entity_registry.view(Surface) @property def mirrored_body_groups(self) -> EntityRegistryView: """ Return the list of mirrored body groups in the draft. Notes ----- Mirrored entities are draft-only entities derived from mirror actions and stored in the draft registry. """ return self._entity_registry.view(MirroredGeometryBodyGroup) @property def mirrored_surfaces(self) -> EntityRegistryView: """ Return the list of mirrored surfaces in the draft. Notes ----- Mirrored entities are draft-only entities derived from mirror actions and stored in the draft registry. """ return self._entity_registry.view(MirroredSurface) @property def edges(self) -> EntityRegistryView: """ Return the list of edges in the draft. """ return self._entity_registry.view(Edge) @property def volumes(self) -> EntityRegistryView: """ Return the list of volumes (volume zones) in the draft. """ return self._entity_registry.view(GenericVolume) # Non-persistent entities @property def boxes(self) -> EntityRegistryView: """ Return the list of boxes in the draft. """ # pylint: disable=import-outside-toplevel from flow360.component.simulation.primitives import Box return self._entity_registry.view(Box) @property def cylinders(self) -> EntityRegistryView: """ Return the list of cylinders in the draft. """ # pylint: disable=import-outside-toplevel from flow360.component.simulation.primitives import Cylinder return self._entity_registry.view(Cylinder) @property def imported_geometries(self) -> List: """ Return the list of imported geometries in the draft. """ return self._imported_geometries @property def imported_surfaces(self) -> EntityRegistryView: """ Return the list of imported surfaces in the draft. """ return self._entity_registry.view(ImportedSurface) @property def coordinate_systems(self) -> CoordinateSystemManager: """ Coordinate system manager for this draft. This is the primary user entry point to create/remove coordinate systems, define parent relationships, and assign coordinate systems to draft entities. See Also -------- CoordinateSystemManager """ return self._coordinate_system_manager @property def mirror(self) -> MirrorManager: """ Mirror manager for this draft. This is the primary user entry point to define mirror planes and create/remove mirrored draft-only entities derived from geometry body groups. See Also -------- MirrorManager """ return self._mirror_manager
[docs] def preview_selector(self, selector: "EntitySelector", *, return_names: bool = True): """ Preview which entities a selector would match in this draft context. Parameters ---------- selector : EntitySelector The selector to preview (SurfaceSelector, EdgeSelector, VolumeSelector, or BodyGroupSelector). return_names : bool, default True When True, returns entity names. When False, returns entity instances. Returns ------- list[str] or list[EntityBase] Matched entity names or instances depending on ``return_names``. Example ------- >>> import flow360 as fl >>> geometry = fl.Geometry.from_cloud(id="...") >>> with fl.create_draft(new_run_from=geometry) as draft: ... selector = fl.SurfaceSelector(name="wing_surfaces").match("wing*") ... matched = draft.preview_selector(selector) ... print(matched) # ['wing_upper', 'wing_lower', ...] """ # pylint: disable=import-outside-toplevel log.warning( "!!! This is a beta feature and may be removed or changed in future releases. !!!" ) from flow360.component.simulation.framework.entity_selector import ( expand_entity_list_selectors, ) if not isinstance(selector, EntitySelector): raise Flow360ValueError( f"Expected EntitySelector, got {type(selector).__name__}. " "Use fl.SurfaceSelector, fl.EdgeSelector, fl.VolumeSelector, or fl.BodyGroupSelector." ) @dataclass class MockEntityList: """Temporary mock for EntityList to avoid metaclass constraints.""" selectors: List[EntitySelector] matched_entities = expand_entity_list_selectors( registry=self._entity_registry, entity_list=MockEntityList(selectors=[selector]), ) if not matched_entities: return [] # Return names or instances based on return_names if return_names: return [entity.name for entity in matched_entities] return matched_entities
# endregion ------------------------------------------------------------------------------------