diff --git a/examples/geometry_to_surface_mesh_V2.py b/examples/geometry_to_surface_mesh_V2.py deleted file mode 100644 index 43b0182b5..000000000 --- a/examples/geometry_to_surface_mesh_V2.py +++ /dev/null @@ -1,38 +0,0 @@ -import flow360 as fl -from flow360.component.geometry import Geometry -from flow360.component.simulation import cloud -from flow360.component.simulation.meshing_param.face_params import ( - BoundaryLayer, - SurfaceRefinement, -) -from flow360.component.simulation.meshing_param.params import ( - MeshingDefaults, - MeshingParams, -) -from flow360.component.simulation.meshing_param.volume_params import AutomatedFarfield -from flow360.component.simulation.simulation_params import SimulationParams -from flow360.component.simulation.unit_system import SI_unit_system -from flow360.examples import Airplane - -fl.Env.dev.active() - -# geometry_draft = Geometry.from_file( -# Airplane.geometry, solver_version="workbenchMeshGrouping-24.9.1" -# ) -# geometry = geometry_draft.submit() -geometry = Geometry.from_cloud("geo-e89fe565-24a2-4777-b563-2ec1d3d2a133") -# geometry.show_available_groupings(verbose_mode=True) -geometry.group_faces_by_tag(tag_name="groupName") -with SI_unit_system: - params = SimulationParams( - meshing=MeshingParams( - defaults=MeshingDefaults( - boundary_layer_first_layer_thickness=1, - surface_edge_growth_rate=1.4, - surface_max_edge_length=1.0111, - ), - volume_zones=[AutomatedFarfield()], - ), - ) -cloud.generate_surface_mesh(geometry, params=params, draft_name="TestGrouping", async_mode=False) -# print(geometry._meta_class) diff --git a/examples/project_from_cloud_geometry.py b/examples/project_from_cloud_geometry.py new file mode 100644 index 000000000..e6c3bc8e3 --- /dev/null +++ b/examples/project_from_cloud_geometry.py @@ -0,0 +1,49 @@ +from flow360.component.project import Project +from flow360.component.simulation.meshing_param.params import ( + MeshingDefaults, + MeshingParams, +) +from flow360.component.simulation.meshing_param.volume_params import AutomatedFarfield +from flow360.component.simulation.models.surface_models import Freestream, Wall +from flow360.component.simulation.operating_condition.operating_condition import ( + AerospaceCondition, +) +from flow360.component.simulation.outputs.outputs import SurfaceOutput +from flow360.component.simulation.primitives import ReferenceGeometry +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.component.simulation.time_stepping.time_stepping import Steady +from flow360.component.simulation.unit_system import SI_unit_system, u +from flow360.environment import dev + +dev.active() + +project = Project.from_cloud("prj-f3569ba5-16a3-4e41-bfd2-b8840df79835") + +geometry = project.geometry +geometry.show_available_groupings(verbose_mode=True) +geometry.group_faces_by_tag("faceId") + +with SI_unit_system: + params = SimulationParams( + meshing=MeshingParams( + defaults=MeshingDefaults( + boundary_layer_first_layer_thickness=0.001, surface_max_edge_length=1 + ), + volume_zones=[AutomatedFarfield()], + ), + reference_geometry=ReferenceGeometry(), + operating_condition=AerospaceCondition(velocity_magnitude=100, alpha=5 * u.deg), + time_stepping=Steady(max_steps=1000), + models=[ + Wall( + surfaces=[geometry["*"]], + name="Wall", + ), + Freestream(surfaces=[AutomatedFarfield().farfield], name="Freestream"), + ], + outputs=[ + SurfaceOutput(surfaces=geometry["*"], output_fields=["Cp", "Cf", "yPlus", "CfVec"]) + ], + ) + +project.run_case(params=params, name="Case of Simple Airplane from Python") diff --git a/examples/project_from_cloud_volume_mesh.py b/examples/project_from_cloud_volume_mesh.py new file mode 100644 index 000000000..84cf27bbc --- /dev/null +++ b/examples/project_from_cloud_volume_mesh.py @@ -0,0 +1,37 @@ +from matplotlib.pyplot import show + +import flow360.component.simulation.units as u +from flow360.component.project import Project +from flow360.component.simulation.models.surface_models import ( + Freestream, + SymmetryPlane, + Wall, +) +from flow360.component.simulation.operating_condition.operating_condition import ( + AerospaceCondition, +) +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.component.simulation.unit_system import SI_unit_system +from flow360.environment import dev + +dev.active() + +project = Project.from_cloud("prj-b8eb4cc7-4fb8-4baa-9bcd-f1cf6d73163d") + +volume_mesh = project.volume_mesh + +with SI_unit_system: + params = SimulationParams( + operating_condition=AerospaceCondition(velocity_magnitude=100 * u.m / u.s), + models=[ + Wall(entities=[volume_mesh["1"]]), + Freestream(entities=[volume_mesh["3"]]), + SymmetryPlane(entities=[volume_mesh["2"]]), + ], + ) + +project.run_case(params=params) + +residuals = project.case.results.nonlinear_residuals +residuals.as_dataframe().plot(x="pseudo_step", logy=True) +show() diff --git a/examples/project_from_file_geometry.py b/examples/project_from_file_geometry.py new file mode 100644 index 000000000..e3923550c --- /dev/null +++ b/examples/project_from_file_geometry.py @@ -0,0 +1,50 @@ +import flow360 as fl +from flow360.component.project import Project +from flow360.component.simulation.meshing_param.params import ( + MeshingDefaults, + MeshingParams, +) +from flow360.component.simulation.meshing_param.volume_params import AutomatedFarfield +from flow360.component.simulation.models.surface_models import Freestream, Wall +from flow360.component.simulation.operating_condition.operating_condition import ( + AerospaceCondition, +) +from flow360.component.simulation.outputs.outputs import SurfaceOutput +from flow360.component.simulation.primitives import ReferenceGeometry +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.component.simulation.time_stepping.time_stepping import Steady +from flow360.component.simulation.unit_system import SI_unit_system, u +from flow360.examples import Airplane + +fl.Env.dev.active() + +project = Project.from_file(Airplane.geometry, name="Python Project (Geometry, from file)") + +geometry = project.geometry +geometry.show_available_groupings(verbose_mode=True) +geometry.group_faces_by_tag("groupName") + +with SI_unit_system: + params = SimulationParams( + meshing=MeshingParams( + defaults=MeshingDefaults( + boundary_layer_first_layer_thickness=0.001, surface_max_edge_length=1 + ), + volume_zones=[AutomatedFarfield()], + ), + reference_geometry=ReferenceGeometry(), + operating_condition=AerospaceCondition(velocity_magnitude=100, alpha=5 * u.deg), + time_stepping=Steady(max_steps=1000), + models=[ + Wall( + surfaces=[geometry["*"]], + name="Wall", + ), + Freestream(surfaces=[AutomatedFarfield().farfield], name="Freestream"), + ], + outputs=[ + SurfaceOutput(surfaces=geometry["*"], output_fields=["Cp", "Cf", "yPlus", "CfVec"]) + ], + ) + +project.run_case(params=params, name="Case of Simple Airplane from Python") diff --git a/examples/project_from_file_geometry_multiple_runs.py b/examples/project_from_file_geometry_multiple_runs.py new file mode 100644 index 000000000..254136c8b --- /dev/null +++ b/examples/project_from_file_geometry_multiple_runs.py @@ -0,0 +1,83 @@ +import flow360 as fl +from flow360.component.project import Project +from flow360.component.simulation.meshing_param.params import ( + MeshingDefaults, + MeshingParams, +) +from flow360.component.simulation.meshing_param.volume_params import ( + AutomatedFarfield, + UniformRefinement, +) +from flow360.component.simulation.models.surface_models import Freestream, Wall +from flow360.component.simulation.operating_condition.operating_condition import ( + AerospaceCondition, +) +from flow360.component.simulation.outputs.outputs import SurfaceOutput +from flow360.component.simulation.primitives import Box, ReferenceGeometry +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.component.simulation.time_stepping.time_stepping import Steady +from flow360.component.simulation.unit_system import SI_unit_system, u +from flow360.examples import Airplane + +fl.Env.dev.active() + +project = Project.from_file( + Airplane.geometry, name="Python Project (Geometry, from file, multiple runs)" +) + +geometry = project.geometry +geometry.show_available_groupings(verbose_mode=True) +geometry.group_faces_by_tag("groupName") + +with SI_unit_system: + params = SimulationParams( + meshing=MeshingParams( + defaults=MeshingDefaults( + boundary_layer_first_layer_thickness=0.001, surface_max_edge_length=1 + ), + volume_zones=[AutomatedFarfield()], + refinements=[ + UniformRefinement( + entities=[ + Box.from_principal_axes( + name="MyBox", + center=(0, 1, 2), + size=(4, 5, 6), + axes=((2, 2, 0), (-2, 2, 0)), + ), + ], + spacing=1.5, + ), + ], + ), + reference_geometry=ReferenceGeometry(), + operating_condition=AerospaceCondition(velocity_magnitude=100, alpha=5 * u.deg), + time_stepping=Steady(max_steps=1000), + models=[ + Wall( + surfaces=[geometry["*"]], + name="Wall", + ), + Freestream(surfaces=[AutomatedFarfield().farfield], name="Freestream"), + ], + outputs=[ + SurfaceOutput(surfaces=geometry["*"], output_fields=["Cp", "Cf", "yPlus", "CfVec"]) + ], + ) + +# Run the mesher once +project.generate_surface_mesh(params=params, name="Surface mesh 1") +surface_mesh_1 = project.surface_mesh + +# Tweak some parameter in the params +params.meshing.defaults.surface_max_edge_length = 2 * u.m + +# Run the mesher again +project.generate_surface_mesh(params=params, name="Surface mesh 2") +surface_mesh_2 = project.surface_mesh + +assert surface_mesh_1.id != surface_mesh_2.id + +# Check available surface mesh IDs in the project +ids = project.get_cached_surface_meshes() +print(ids) diff --git a/examples/project_from_file_volume_mesh.py b/examples/project_from_file_volume_mesh.py new file mode 100644 index 000000000..86c4b5089 --- /dev/null +++ b/examples/project_from_file_volume_mesh.py @@ -0,0 +1,42 @@ +from matplotlib.pyplot import show + +import flow360 as fl +import flow360.component.simulation.units as u +from flow360.component.project import Project +from flow360.component.simulation.models.surface_models import ( + Freestream, + SymmetryPlane, + Wall, +) +from flow360.component.simulation.operating_condition.operating_condition import ( + AerospaceCondition, +) +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.component.simulation.unit_system import SI_unit_system +from flow360.examples import OM6wing + +fl.Env.dev.active() + +OM6wing.get_files() +# Creating and uploading a volume mesh from file +project = Project.from_file( + OM6wing.mesh_filename, name="wing-volume-mesh-python-upload", tags=["python"] +) + +volume_mesh = project.volume_mesh + +with SI_unit_system: + params = SimulationParams( + operating_condition=AerospaceCondition(velocity_magnitude=100 * u.m / u.s), + models=[ + Wall(entities=[volume_mesh["1"]]), + Freestream(entities=[volume_mesh["3"]]), + SymmetryPlane(entities=[volume_mesh["2"]]), + ], + ) + +project.run_case(params=params) + +residuals = project.case.results.nonlinear_residuals +residuals.as_dataframe().plot(x="pseudo_step", logy=True) +show() diff --git a/flow360/__init__.py b/flow360/__init__.py index 358ecc9a7..bb647ff0c 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -145,7 +145,7 @@ from .environment import Env from .flags import Flags from .user_config import UserConfig -from .version import __version__ +from .version import __solver_version__, __version__ __all__ = [ "Accounts", @@ -262,6 +262,7 @@ "ZeroFreestream", "ZeroFreestreamFromVelocity", "__version__", + "__solver_version__", "air", "flow360", "flow360_unit_system", diff --git a/flow360/component/case.py b/flow360/component/case.py index d104f6b40..651be2048 100644 --- a/flow360/component/case.py +++ b/flow360/component/case.py @@ -6,7 +6,6 @@ import json import tempfile -import time from typing import Any, Iterator, List, Union import pydantic.v1 as pd @@ -524,12 +523,6 @@ def has_user_defined_dynamics(self): """ return self.params.user_defined_dynamics is not None - def is_finished(self): - """ - returns False when case is in running or preprocessing state - """ - return self.status.is_final() - def move_to_folder(self, folder: Folder): """ Move the current case to the specified folder. @@ -626,17 +619,6 @@ def create( ) return new_case - def wait(self, timeout_minutes=60): - """Wait until the Case finishes processing, refresh periodically""" - - start_time = time.time() - while self.is_finished() is False: - if time.time() - start_time > timeout_minutes * 60: - raise TimeoutError( - "Timeout: Process did not finish within the specified timeout period" - ) - time.sleep(2) - # pylint: disable=unnecessary-lambda class CaseResultsModel(pd.BaseModel): diff --git a/flow360/component/geometry.py b/flow360/component/geometry.py index b0dee02ee..de8f6dd04 100644 --- a/flow360/component/geometry.py +++ b/flow360/component/geometry.py @@ -23,7 +23,7 @@ from flow360.component.simulation.entity_info import GeometryEntityInfo from flow360.component.simulation.framework.entity_registry import EntityRegistry from flow360.component.simulation.primitives import Edge, Surface -from flow360.component.simulation.utils import _model_attribute_unlock +from flow360.component.simulation.utils import model_attribute_unlock from flow360.component.simulation.web.asset_base import AssetBase from flow360.component.utils import ( SUPPORTED_GEOMETRY_FILE_PATTERNS, @@ -223,7 +223,7 @@ def face_group_tag(self): @face_group_tag.setter def face_group_tag(self, new_value: str): - with _model_attribute_unlock(self._entity_info, "face_group_tag"): + with model_attribute_unlock(self._entity_info, "face_group_tag"): self._entity_info.face_group_tag = new_value @property @@ -233,7 +233,7 @@ def edge_group_tag(self): @edge_group_tag.setter def edge_group_tag(self, new_value: str): - with _model_attribute_unlock(self._entity_info, "edge_group_tag"): + with model_attribute_unlock(self._entity_info, "edge_group_tag"): self._entity_info.edge_group_tag = new_value @classmethod diff --git a/flow360/component/project.py b/flow360/component/project.py new file mode 100644 index 000000000..a12dd0c5d --- /dev/null +++ b/flow360/component/project.py @@ -0,0 +1,720 @@ +"""Project interface for setting up and running simulations""" + +# pylint: disable=no-member +# To be honest I do not know why pylint is insistent on treating +# ProjectMeta instances as FieldInfo, I'd rather not have this line +import json +from enum import Enum +from typing import Iterable, List, Optional, Union + +import pydantic as pd + +from flow360 import Case, SurfaceMesh, __solver_version__ +from flow360.cloud.requests import LengthUnitType +from flow360.cloud.rest_api import RestApi +from flow360.component.geometry import Geometry +from flow360.component.interfaces import ( + GeometryInterface, + ProjectInterface, + VolumeMeshInterfaceV2, +) +from flow360.component.resource_base import Flow360Resource +from flow360.component.simulation.outputs.output_entities import Point, Slice +from flow360.component.simulation.primitives import Box, Cylinder +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.component.simulation.unit_system import LengthType +from flow360.component.simulation.utils import model_attribute_unlock +from flow360.component.simulation.web.asset_base import AssetBase +from flow360.component.simulation.web.draft import Draft +from flow360.component.utils import ( + SUPPORTED_GEOMETRY_FILE_PATTERNS, + MeshNameParser, + ProjectAssetCache, + match_file_pattern, +) +from flow360.component.volume_mesh import VolumeMeshV2 +from flow360.exceptions import Flow360FileError, Flow360ValueError, Flow360WebError + +AssetOrResource = Union[type[AssetBase], type[Flow360Resource]] +RootAsset = Union[Geometry, VolumeMeshV2] + + +class RootType(Enum): + """ + Enum for root object types in the project. + + Attributes + ---------- + GEOMETRY : str + Represents a geometry root object. + VOLUME_MESH : str + Represents a volume mesh root object. + """ + + GEOMETRY = "Geometry" + VOLUME_MESH = "VolumeMesh" + + +class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): + """ + Metadata class for a project. + + Attributes + ---------- + user_id : str + The user ID associated with the project. + id : str + The project ID. + name : str + The name of the project. + root_item_id : str + ID of the root item in the project. + root_item_type : RootType + Type of the root item (Geometry or VolumeMesh). + """ + + user_id: str = pd.Field(alias="userId") + id: str = pd.Field() + name: str = pd.Field() + root_item_id: str = pd.Field(alias="rootItemId") + root_item_type: RootType = pd.Field(alias="rootItemType") + + +_SurfaceMeshCache = ProjectAssetCache[SurfaceMesh] +_VolumeMeshCache = ProjectAssetCache[VolumeMeshV2] +_CaseCache = ProjectAssetCache[Case] + + +class Project(pd.BaseModel): + """ + Project class containing the interface for creating and running simulations. + + Attributes + ---------- + metadata : ProjectMeta + Metadata of the project. + solver_version : str + Version of the solver being used. + """ + + metadata: ProjectMeta = pd.Field() + solver_version: str = pd.Field(frozen=True) + + _root_asset: Union[Geometry, VolumeMeshV2] = pd.PrivateAttr(None) + + _volume_mesh_cache: _VolumeMeshCache = pd.PrivateAttr(_VolumeMeshCache()) + _surface_mesh_cache: _SurfaceMeshCache = pd.PrivateAttr(_SurfaceMeshCache()) + _case_cache: _CaseCache = pd.PrivateAttr(_CaseCache()) + + _root_webapi: Optional[RestApi] = pd.PrivateAttr(None) + _project_webapi: Optional[RestApi] = pd.PrivateAttr(None) + _root_simulation_json: Optional[dict] = pd.PrivateAttr(None) + + @property + def geometry(self) -> Geometry: + """ + Returns the geometry asset of the project. There is always only one geometry asset per project. + + Raises + ------ + Flow360ValueError + If the geometry asset is not available for the project. + + Returns + ------- + Geometry + The geometry asset. + """ + self._check_initialized() + if self.metadata.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError( + "Geometry asset is only present in projects initialized from geometry." + ) + + return self._root_asset + + def get_surface_mesh(self, asset_id: str = None) -> SurfaceMesh: + """ + Returns the surface mesh asset of the project. + + Parameters + ---------- + asset_id : str, optional + The ID of the asset from among the generated assets in this project instance. If not provided, + the property contains the most recently run asset. + + Raises + ------ + Flow360ValueError + If the surface mesh asset is not available for the project. + + Returns + ------- + SurfaceMesh + The surface mesh asset. + """ + self._check_initialized() + if self.metadata.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError( + "Surface mesh assets are only present in projects initialized from geometry." + ) + + return self._surface_mesh_cache.get_asset(asset_id) + + @property + def surface_mesh(self): + """ + Returns the last used surface mesh asset of the project. + + Raises + ------ + Flow360ValueError + If the surface mesh asset is not available for the project. + + Returns + ------- + SurfaceMesh + The surface mesh asset. + """ + return self.get_surface_mesh() + + def get_volume_mesh(self, asset_id: str = None) -> VolumeMeshV2: + """ + Returns the volume mesh asset of the project. + + Parameters + ---------- + asset_id : str, optional + The ID of the asset from among the generated assets in this project instance. If not provided, + the property contains the most recently run asset. + + Raises + ------ + Flow360ValueError + If the volume mesh asset is not available for the project. + + Returns + ------- + VolumeMeshV2 + The volume mesh asset. + """ + self._check_initialized() + if self.metadata.root_item_type is RootType.VOLUME_MESH: + if asset_id is not None: + raise Flow360ValueError( + "Cannot retrieve volume meshes by asset ID in a project created from volume mesh, " + "there is only one root volume mesh asset in this project. Use project.volume_mesh()." + ) + + return self._root_asset + + return self._volume_mesh_cache.get_asset(asset_id) + + @property + def volume_mesh(self): + """ + Returns the last used volume mesh asset of the project. + + Raises + ------ + Flow360ValueError + If the volume mesh asset is not available for the project. + + Returns + ------- + VolumeMeshV2 + The volume mesh asset. + """ + return self.get_volume_mesh() + + def get_case(self, asset_id: str = None) -> Case: + """ + Returns the last used case asset of the project. + + Parameters + ---------- + asset_id : str, optional + The ID of the asset from among the generated assets in this project instance. If not provided, + the property contains the most recently run asset. + + Raises + ------ + Flow360ValueError + If the case asset is not available for the project. + + Returns + ------- + Case + The case asset. + """ + self._check_initialized() + return self._case_cache.get_asset(asset_id) + + @property + def case(self): + """ + Returns the case asset of the project. + + Raises + ------ + Flow360ValueError + If the case asset is not available for the project. + + Returns + ------- + Case + The case asset. + """ + return self.get_case() + + def get_cached_surface_meshes(self) -> Iterable[str]: + """ + Returns the available IDs of surface meshes in the project + + Returns + ------- + Iterable[str] + An iterable of asset IDs. + """ + if self.metadata.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError( + "Surface mesh assets are only present in objects initialized from geometry." + ) + + return self._surface_mesh_cache.get_ids() + + def get_cached_volume_meshes(self): + """ + Returns the available IDs of volume meshes in the project + + Returns + ------- + Iterable[str] + An iterable of asset IDs. + """ + if self.metadata.root_item_type is RootType.VOLUME_MESH: + raise Flow360ValueError( + "Cannot retrieve volume meshes by asset ID in a project created from volume mesh, " + "there is only one root volume mesh asset in this project. Use project.volume_mesh()." + ) + + return self._volume_mesh_cache.get_ids() + + def get_cached_cases(self): + """ + Returns the available IDs of cases in the project + + Returns + ------- + Iterable[str] + An iterable of asset IDs. + """ + return self._case_cache.get_ids() + + @classmethod + def _detect_asset_type_from_file(cls, file): + """ + Detects the asset type of a file based on its name or pattern. + + Parameters + ---------- + file : str + The file name or path. + + Returns + ------- + RootType + The detected root type (Geometry or VolumeMesh). + + Raises + ------ + Flow360FileError + If the file does not match any known patterns. + """ + if match_file_pattern(SUPPORTED_GEOMETRY_FILE_PATTERNS, file): + return RootType.GEOMETRY + try: + parser = MeshNameParser(file) + if parser.is_valid_volume_mesh(): + return RootType.VOLUME_MESH + except Flow360FileError: + pass + + raise Flow360FileError( + f"{file} is not a geometry or volume mesh file required for project initialization. " + "Accepted formats are: " + f"{SUPPORTED_GEOMETRY_FILE_PATTERNS} (geometry)" + f"{MeshNameParser.all_patterns(mesh_type='volume')} (volume mesh)" + ) + + # pylint: disable=too-many-arguments + @classmethod + @pd.validate_call + def from_file( + cls, + file: str = None, + name: str = None, + solver_version: str = __solver_version__, + length_unit: LengthUnitType = "m", + tags: List[str] = None, + ): + """ + Initializes a project from a file. + + Parameters + ---------- + file : str + Path to the file. + name : str, optional + Name of the project (default is None). + solver_version : str, optional + Version of the solver (default is None). + length_unit : LengthUnitType, optional + Unit of length (default is "m"). + tags : list of str, optional + Tags to assign to the project (default is None). + + Returns + ------- + Project + An instance of the project. + + Raises + ------ + Flow360ValueError + If the project cannot be initialized from the file. + """ + root_asset = None + root_type = cls._detect_asset_type_from_file(file) + if root_type == RootType.GEOMETRY: + draft = Geometry.from_file(file, name, solver_version, length_unit, tags) + root_asset = draft.submit() + elif root_type == RootType.VOLUME_MESH: + draft = VolumeMeshV2.from_file(file, name, solver_version, length_unit, tags) + root_asset = draft.submit() + if not root_asset: + raise Flow360ValueError(f"Couldn't initialize asset from {file}") + project_id = root_asset.project_id + project_api = RestApi(ProjectInterface.endpoint, id=project_id) + info = project_api.get() + if not info: + raise Flow360ValueError(f"Couldn't retrieve project info for {project_id}") + project = Project(metadata=ProjectMeta(**info), solver_version=root_asset.solver_version) + project._project_webapi = project_api + if root_type == RootType.GEOMETRY: + project._root_asset = root_asset + project._root_webapi = RestApi(GeometryInterface.endpoint, id=root_asset.id) + elif root_type == RootType.VOLUME_MESH: + project._root_asset = root_asset + project._root_webapi = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_asset.id) + project._get_root_simulation_json() + return project + + @classmethod + @pd.validate_call + def from_cloud(cls, project_id: str): + """ + Loads a project from the cloud. + + Parameters + ---------- + project_id : str + ID of the project. + + Returns + ------- + Project + An instance of the project. + + Raises + ------ + Flow360WebError + If the project cannot be loaded from the cloud. + Flow360ValueError + If the root asset cannot be retrieved for the project. + """ + project_api = RestApi(ProjectInterface.endpoint, id=project_id) + info = project_api.get() + if not isinstance(info, dict): + raise Flow360WebError(f"Cannot load project {project_id}, missing project data.") + if not info: + raise Flow360WebError(f"Couldn't retrieve project info for {project_id}") + meta = ProjectMeta(**info) + root_asset = None + root_type = meta.root_item_type + if root_type == RootType.GEOMETRY: + root_asset = Geometry.from_cloud(meta.root_item_id) + elif root_type == RootType.VOLUME_MESH: + root_asset = VolumeMeshV2.from_cloud(meta.root_item_id) + if not root_asset: + raise Flow360ValueError(f"Couldn't retrieve root asset for {project_id}") + project = Project(metadata=meta, solver_version=root_asset.solver_version) + project._project_webapi = project_api + if root_type == RootType.GEOMETRY: + project._root_asset = root_asset + project._root_webapi = RestApi(GeometryInterface.endpoint, id=root_asset.id) + elif root_type == RootType.VOLUME_MESH: + project._root_asset = root_asset + project._root_webapi = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_asset.id) + project._get_root_simulation_json() + return project + + def _check_initialized(self): + """ + Checks if the project instance has been initialized correctly. + + Raises + ------ + Flow360ValueError + If the project is not initialized. + """ + if not self.metadata or not self.solver_version or not self._root_asset: + raise Flow360ValueError( + "Project not initialized - use Project.from_file or Project.from_cloud" + ) + + def _get_root_simulation_json(self): + """ + Loads the default simulation JSON for the project based on the root asset type. + + Raises + ------ + Flow360ValueError + If the root item type or ID is missing from project metadata. + Flow360WebError + If the simulation JSON cannot be retrieved. + """ + self._check_initialized() + root_type = self.metadata.root_item_type + root_id = self.metadata.root_item_id + if not root_type or not root_id: + raise Flow360ValueError("Root item type or ID is missing from project metadata") + resp = self._root_webapi.get(method="simulation/file", params={"type": "simulation"}) + if not isinstance(resp, dict) or "simulationJson" not in resp: + raise Flow360WebError("Couldn't retrieve default simulation JSON for the project") + simulation_json = json.loads(resp["simulationJson"]) + self._root_simulation_json = simulation_json + + # pylint: disable=too-many-arguments, too-many-locals + def _run( + self, + params: SimulationParams, + target: AssetOrResource, + draft_name: str = None, + fork: bool = False, + run_async: bool = True, + solver_version: str = None, + ): + """ + Runs a simulation for the project. + + Parameters + ---------- + params : SimulationParams + The simulation parameters to use for the run + target : AssetOrResource + The target asset or resource to run the simulation against. + draft_name : str, optional + The name of the draft to create for the simulation run (default is None). + fork : bool, optional + Indicates if the simulation should fork the existing case (default is False). + run_async : bool, optional + Specifies whether the simulation should run asynchronously (default is True). + + Returns + ------- + AssetOrResource + The destination asset + + Raises + ------ + Flow360ValueError + If the simulation parameters lack required length unit information, or if the + root asset (Geometry or VolumeMesh) is not initialized. + """ + + defaults = self._root_simulation_json + + cache_key = "private_attribute_asset_cache" + length_key = "project_length_unit" + + if cache_key not in defaults: + if length_key not in defaults[cache_key]: + raise Flow360ValueError("Simulation params do not contain default length unit info") + + length_unit = defaults[cache_key][length_key] + + with model_attribute_unlock(params.private_attribute_asset_cache, length_key): + params.private_attribute_asset_cache.project_length_unit = LengthType.validate( + length_unit + ) + + root_asset = self._root_asset + + draft = Draft.create( + name=draft_name, + project_id=self.metadata.id, + source_item_id=self.metadata.root_item_id, + source_item_type=self.metadata.root_item_type.value, + solver_version=solver_version if solver_version else self.solver_version, + fork_case=fork, + ).submit() + + entity_registry = params.used_entity_registry + + # Check if there are any new draft entities that have been added in the params by the user + entity_info = root_asset.entity_info + for draft_type in [Box, Cylinder, Point, Slice]: + draft_entities = entity_registry.find_by_type(draft_type) + for draft_entity in draft_entities: + if draft_entity not in entity_info.draft_entities: + entity_info.draft_entities.append(draft_entity) + + with model_attribute_unlock(params.private_attribute_asset_cache, "project_entity_info"): + params.private_attribute_asset_cache.project_entity_info = entity_info + + draft.update_simulation_params(params) + destination_id = draft.run_up_to_target_asset(target) + + self._project_webapi.patch( + json={ + "lastOpenItemId": destination_id, + "lastOpenItemType": target.__name__, + } + ) + + destination_obj = target.from_cloud(destination_id) + + if not run_async: + destination_obj.wait() + + return destination_obj + + @pd.validate_call + def generate_surface_mesh( + self, + params: SimulationParams, + name: str = "SurfaceMesh", + run_async: bool = True, + fork: bool = False, + solver_version: str = None, + ): + """ + Runs the surface mesher for the project. + + Parameters + ---------- + params : SimulationParams + Simulation parameters for running the mesher. + name : str, optional + Name of the draft (default is "SurfaceMesh"). + run_async : bool, optional + Whether to run the mesher asynchronously (default is True). + fork : bool, optional + Whether to fork the case (default is False). + solver_version : str, optional + Optional solver version to use during this run (defaults to the project solver version) + + Raises + ------ + Flow360ValueError + If the root item type is not Geometry. + """ + self._check_initialized() + if self.metadata.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError( + "Surface mesher can only be run by projects with a geometry root asset" + ) + self._surface_mesh_cache.add_asset( + self._run( + params=params, + target=SurfaceMesh, + draft_name=name, + run_async=run_async, + fork=fork, + solver_version=solver_version, + ) + ) + + @pd.validate_call + def generate_volume_mesh( + self, + params: SimulationParams, + name: str = "VolumeMesh", + run_async: bool = True, + fork: bool = True, + solver_version: str = None, + ): + """ + Runs the volume mesher for the project. + + Parameters + ---------- + params : SimulationParams + Simulation parameters for running the mesher. + name : str, optional + Name of the draft (default is "VolumeMesh"). + run_async : bool, optional + Whether to run the mesher asynchronously (default is True). + fork : bool, optional + Whether to fork the case (default is True). + solver_version : str, optional + Optional solver version to use during this run (defaults to the project solver version) + + Raises + ------ + Flow360ValueError + If the root item type is not Geometry. + """ + self._check_initialized() + if self.metadata.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError( + "Volume mesher can only be run by projects with a geometry root asset" + ) + self._volume_mesh_cache.add_asset( + self._run( + params=params, + target=VolumeMeshV2, + draft_name=name, + run_async=run_async, + fork=fork, + solver_version=solver_version, + ) + ) + + @pd.validate_call + def run_case( + self, + params: SimulationParams, + draft_name: str = "Case", + run_async: bool = True, + fork: bool = True, + solver_version: str = None, + ): + """ + Runs a case for the project. + + Parameters + ---------- + params : SimulationParams + Simulation parameters for running the case. + draft_name : str, optional + Name of the draft (default is "Case"). + run_async : bool, optional + Whether to run the case asynchronously (default is True). + fork : bool, optional + Whether to fork the case (default is True). + solver_version : str, optional + Optional solver version to use during this run (defaults to the project solver version) + """ + self._check_initialized() + self._case_cache.add_asset( + self._run( + params=params, + target=Case, + draft_name=draft_name, + run_async=run_async, + fork=fork, + solver_version=solver_version, + ) + ) diff --git a/flow360/component/resource_base.py b/flow360/component/resource_base.py index 10cb7d38f..73de52164 100644 --- a/flow360/component/resource_base.py +++ b/flow360/component/resource_base.py @@ -5,6 +5,7 @@ import os import re import shutil +import time import traceback from abc import ABCMeta from datetime import datetime @@ -265,6 +266,17 @@ def name(self): """ return self.info.name + def wait(self, timeout_minutes=60): + """Wait until the Resource finishes processing, refresh periodically""" + + start_time = time.time() + while self.status.is_final() is False: + if time.time() - start_time > timeout_minutes * 60: + raise TimeoutError( + "Timeout: Process did not finish within the specified timeout period" + ) + time.sleep(2) + def short_description(self) -> str: """short_description diff --git a/flow360/component/simulation/cloud.py b/flow360/component/simulation/cloud.py deleted file mode 100644 index a47abf388..000000000 --- a/flow360/component/simulation/cloud.py +++ /dev/null @@ -1,141 +0,0 @@ -"""Cloud communication for simulation related tasks""" - -from __future__ import annotations - -import time - -from flow360.cloud.rest_api import RestApi -from flow360.component.case import Case -from flow360.component.interfaces import ProjectInterface -from flow360.component.simulation.simulation_params import SimulationParams -from flow360.component.simulation.unit_system import LengthType -from flow360.component.simulation.utils import _model_attribute_unlock -from flow360.component.simulation.web.asset_base import AssetBase -from flow360.component.simulation.web.draft import Draft -from flow360.component.surface_mesh import SurfaceMesh -from flow360.component.volume_mesh import VolumeMesh -from flow360.log import log - -TIMEOUT_MINUTES = 60 - - -def _get_source_type_string(source_asset_class_name: str): - if source_asset_class_name.endswith("V2"): - return source_asset_class_name[:-2] # Removes the last two characters (i.e., "V2") - return source_asset_class_name - - -def _check_project_path_status(project_id: str, item_id: str, item_type: str) -> None: - RestApi(ProjectInterface.endpoint, id=project_id).get( - method="path", params={"itemId": item_id, "itemType": item_type} - ) - # pylint: disable=fixme - # TODO: check all status on the given path - - -# pylint: disable=too-many-arguments -def _run( - source_asset: AssetBase, - params: SimulationParams, - target_asset: type[AssetBase], - draft_name: str = None, - fork_case: bool = False, - async_mode: bool = True, -) -> AssetBase: - """ - Generate surface mesh with given simulation params. - async_mode: if True, returns SurfaceMesh object immediately, otherwise waits for the meshing to finish. - """ - if not isinstance(params, SimulationParams): - raise ValueError( - f"params argument must be a SimulationParams object but is of type {type(params)}" - ) - - ##-- Getting the project length unit from the current resource and store in the SimulationParams - # pylint: disable=protected-access - simulation_dict = AssetBase._get_simulation_json(source_asset) - - if ( - "private_attribute_asset_cache" not in simulation_dict - or "project_length_unit" not in simulation_dict["private_attribute_asset_cache"] - ): - raise KeyError( - "[Internal] Could not find project length unit in the draft's simulation settings." - ) - - length_unit = simulation_dict["private_attribute_asset_cache"]["project_length_unit"] - with _model_attribute_unlock(params.private_attribute_asset_cache, "project_length_unit"): - # pylint: disable=no-member - params.private_attribute_asset_cache.project_length_unit = LengthType.validate(length_unit) - - source_item_type = _get_source_type_string(source_asset.__class__.__name__) - ##-- Get new draft - _draft = Draft.create( - name=draft_name, - project_id=source_asset.project_id, - source_item_id=source_asset.id, - source_item_type=source_item_type, - solver_version=source_asset.solver_version, - fork_case=fork_case, - ).submit() - - ##-- Store the entity info part for future retrival - # pylint: disable=protected-access - params = source_asset._inject_entity_info_to_params(params) - - ##-- Post the simulation param: - _draft.update_simulation_params(params) - - ##-- Kick off draft run: - destination_id = _draft.run_up_to_target_asset(target_asset) - - ##-- Patch project - RestApi(ProjectInterface.endpoint, id=source_asset.project_id).patch( - json={ - "lastOpenItemId": destination_id, - "lastOpenItemType": target_asset.__name__, - } - ) - destination_obj = target_asset.from_cloud(destination_id) - - if async_mode is False: - start_time = time.time() - while destination_obj._webapi.status.is_final() is False: - if time.time() - start_time > TIMEOUT_MINUTES * 60: - raise TimeoutError( - "Timeout: Process did not finish within the specified timeout period" - ) - _check_project_path_status(source_asset.project_id, source_asset.id, source_item_type) - log.info("Waiting for the process to finish...") - time.sleep(2) - return destination_obj - - -def generate_surface_mesh( - source_asset: AssetBase, - params: SimulationParams, - draft_name: str = None, - async_mode: bool = True, -): - """generate surface mesh from the geometry""" - return _run(source_asset, params, SurfaceMesh, draft_name, False, async_mode) - - -def generate_volume_mesh( - source_asset: AssetBase, - params: SimulationParams, - draft_name: str = None, - async_mode: bool = True, -): - """generate volume mesh from the geometry""" - return _run(source_asset, params, VolumeMesh, draft_name, False, async_mode) - - -def run_case( - source_asset: AssetBase, - params: SimulationParams, - draft_name: str = None, - async_mode: bool = True, -): - """run case from the geometry""" - return _run(source_asset, params, Case, draft_name, False, async_mode) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index d2af664f3..fc003c661 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -15,7 +15,7 @@ Surface, ) -DraftEntityTypess = Annotated[ +DraftEntityTypes = Annotated[ Union[Box, Cylinder, Point, Slice], pd.Field(discriminator="private_attribute_entity_type_name"), ] @@ -34,7 +34,7 @@ class EntityInfoModel(pd.BaseModel, metaclass=ABCMeta): ) # Storing entities that appeared in the simulation JSON. (Otherwise when front end loads the JSON it will delete # entities that appear in simulation JSON but did not appear in EntityInfo) - draft_entities: List[DraftEntityTypess] = pd.Field([]) + draft_entities: List[DraftEntityTypes] = pd.Field([]) @abstractmethod def get_boundaries(self, attribute_name: str = None) -> list: diff --git a/flow360/component/simulation/framework/multi_constructor_model_base.py b/flow360/component/simulation/framework/multi_constructor_model_base.py index 534572dcb..1ef59c7dc 100644 --- a/flow360/component/simulation/framework/multi_constructor_model_base.py +++ b/flow360/component/simulation/framework/multi_constructor_model_base.py @@ -13,7 +13,7 @@ import pydantic as pd from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.utils import _model_attribute_unlock +from flow360.component.simulation.utils import model_attribute_unlock class MultiConstructorBaseModel(Flow360BaseModel, metaclass=abc.ABCMeta): @@ -57,7 +57,7 @@ def wrapper(cls, **kwargs): for k, v in sig.parameters.items() if v.default is not inspect.Parameter.empty } - with _model_attribute_unlock(obj, "private_attribute_input_cache"): + with model_attribute_unlock(obj, "private_attribute_input_cache"): obj.private_attribute_input_cache = obj.private_attribute_input_cache.__class__( # Note: obj.private_attribute_input_cache should not be included here # Note: Because it carries over the previous cached inputs. Whatever the user choose not to specify @@ -67,7 +67,7 @@ def wrapper(cls, **kwargs): **kwargs, # User specified inputs (overwrites defaults) } ) - with _model_attribute_unlock(obj, "private_attribute_constructor"): + with model_attribute_unlock(obj, "private_attribute_constructor"): obj.private_attribute_constructor = func.__name__ return obj diff --git a/flow360/component/simulation/framework/param_utils.py b/flow360/component/simulation/framework/param_utils.py index c45b74a39..1979b7e2a 100644 --- a/flow360/component/simulation/framework/param_utils.py +++ b/flow360/component/simulation/framework/param_utils.py @@ -18,7 +18,7 @@ _VolumeEntityBase, ) from flow360.component.simulation.unit_system import LengthType -from flow360.component.simulation.utils import _model_attribute_unlock +from flow360.component.simulation.utils import model_attribute_unlock class AssetCache(Flow360BaseModel): @@ -108,7 +108,7 @@ def _update_zone_boundaries_with_metadata( """Update zone boundaries with volume mesh metadata.""" for volume_entity in registry.get_bucket(by_type=_VolumeEntityBase).entities: if volume_entity.name in volume_mesh_meta_data["zones"]: - with _model_attribute_unlock(volume_entity, "private_attribute_zone_boundary_names"): + with model_attribute_unlock(volume_entity, "private_attribute_zone_boundary_names"): volume_entity.private_attribute_zone_boundary_names = UniqueStringList( items=volume_mesh_meta_data["zones"][volume_entity.name]["boundaryNames"] ) @@ -128,5 +128,5 @@ def _set_boundary_full_name_with_zone_name( # Note: We need to figure out how to handle this. Otherwise this may result in wrong # Note: zone name getting prepended. continue - with _model_attribute_unlock(surface, "private_attribute_full_name"): + with model_attribute_unlock(surface, "private_attribute_full_name"): surface.private_attribute_full_name = f"{give_zone_name}/{surface.name}" diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index 89fef0102..411ef734a 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -19,7 +19,7 @@ ) from flow360.component.simulation.framework.unique_list import UniqueStringList from flow360.component.simulation.unit_system import AngleType, AreaType, LengthType -from flow360.component.simulation.utils import _model_attribute_unlock +from flow360.component.simulation.utils import model_attribute_unlock from flow360.component.types import Axis @@ -110,9 +110,9 @@ def _update_entity_info_with_metadata(self, volume_mesh_meta_data: dict[str, dic pattern = r"stationaryBlock|fluid" match = re.search(pattern, zone_full_name) if match is not None or entity_name == zone_full_name: - with _model_attribute_unlock(self, "private_attribute_full_name"): + with model_attribute_unlock(self, "private_attribute_full_name"): self.private_attribute_full_name = zone_full_name - with _model_attribute_unlock(self, "private_attribute_zone_boundary_names"): + with model_attribute_unlock(self, "private_attribute_zone_boundary_names"): self.private_attribute_zone_boundary_names = UniqueStringList( items=zone_meta["boundaryNames"] ) @@ -135,7 +135,7 @@ def _update_entity_info_with_metadata(self, volume_mesh_meta_data: dict) -> None """ Update parent zone name once the volume mesh is done. """ - with _model_attribute_unlock(self, "private_attribute_full_name"): + with model_attribute_unlock(self, "private_attribute_full_name"): self.private_attribute_full_name = _get_boundary_full_name( self.name, volume_mesh_meta_data ) diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index 807a280f1..2c30efc7b 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -46,7 +46,7 @@ imperial_unit_system, unit_system_manager, ) -from flow360.component.simulation.utils import _model_attribute_unlock +from flow360.component.simulation.utils import model_attribute_unlock from flow360.component.simulation.validation.validation_context import ( ALL, SURFACE_MESH, @@ -104,7 +104,7 @@ def _store_project_length_unit(length_unit, params: SimulationParams): # Store the length unit so downstream services/pipelines can use it # pylint: disable=fixme # TODO: client does not call this. We need to start using new webAPI for that - with _model_attribute_unlock(params.private_attribute_asset_cache, "project_length_unit"): + with model_attribute_unlock(params.private_attribute_asset_cache, "project_length_unit"): # pylint: disable=assigning-non-slot,no-member params.private_attribute_asset_cache.project_length_unit = LengthType.validate( length_unit diff --git a/flow360/component/simulation/simulation_params.py b/flow360/component/simulation/simulation_params.py index a86b7c94b..99a088e08 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -298,7 +298,8 @@ def _update_entity_private_attrs(self, registry: EntityRegistry) -> EntityRegist return registry - def _get_used_entity_registry(self) -> EntityRegistry: + @property + def used_entity_registry(self) -> EntityRegistry: """ Get a entity registry that collects all the entities used in the simulation. And also try to update the entities now that we have a global view of the simulation. @@ -308,7 +309,6 @@ def _get_used_entity_registry(self) -> EntityRegistry: registry = self._update_entity_private_attrs(registry) return registry - ##:: Internal Util functions def _update_param_with_actual_volume_mesh_meta(self, volume_mesh_meta_data: dict): """ Update the zone info from the actual volume mesh before solver execution. @@ -317,7 +317,7 @@ def _update_param_with_actual_volume_mesh_meta(self, volume_mesh_meta_data: dict Do we also need to update the params when the **surface meshing** is done? """ # pylint:disable=no-member - used_entity_registry = self._get_used_entity_registry() + used_entity_registry = self.used_entity_registry _update_entity_full_name(self, _SurfaceEntityBase, volume_mesh_meta_data) _update_entity_full_name(self, _VolumeEntityBase, volume_mesh_meta_data) _update_zone_boundaries_with_metadata(used_entity_registry, volume_mesh_meta_data) diff --git a/flow360/component/simulation/utils.py b/flow360/component/simulation/utils.py index a145258a0..5b5c23657 100644 --- a/flow360/component/simulation/utils.py +++ b/flow360/component/simulation/utils.py @@ -4,7 +4,10 @@ @contextmanager -def _model_attribute_unlock(model, attr: str): +def model_attribute_unlock(model, attr: str): + """ + Helper function to set frozen fields of a pydantic model from internal systems + """ try: # validate_assignment is set to False to allow for the attribute to be modified # Otherwise, the attribute will STILL be frozen and cannot be modified diff --git a/flow360/component/simulation/web/asset_base.py b/flow360/component/simulation/web/asset_base.py index da6d497b2..96efd0944 100644 --- a/flow360/component/simulation/web/asset_base.py +++ b/flow360/component/simulation/web/asset_base.py @@ -16,13 +16,9 @@ ResourceDraft, ) from flow360.component.simulation.entity_info import EntityInfoModel -from flow360.component.simulation.framework.entity_registry import EntityRegistry -from flow360.component.simulation.utils import _model_attribute_unlock from flow360.component.utils import remove_properties_by_name, validate_type from flow360.log import log -TIMEOUT_MINUTES = 60 - class AssetBase(metaclass=ABCMeta): """Base class for resource asset""" @@ -59,15 +55,6 @@ def _from_meta(cls, meta: AssetMetaBaseModel): resource._webapi._set_meta(meta) return resource - def _wait_until_final_status(self): - start_time = time.time() - while self._webapi.status.is_final() is False: - if time.time() - start_time > TIMEOUT_MINUTES * 60: - raise TimeoutError( - "Timeout: Asset did not become avaliable within the specified timeout period" - ) - time.sleep(2) - @classmethod def _get_simulation_json(cls, asset: AssetBase) -> dict: """Get the simulation json AKA birth setting of the asset. Do we want to cache it in the asset object?""" @@ -77,7 +64,7 @@ def _get_simulation_json(cls, asset: AssetBase) -> dict: if asset.id == _resp["rootItemId"]: log.debug("Current asset is project's root item. Waiting for pipeline to finish.") # pylint: disable=protected-access - asset._wait_until_final_status() + asset.wait() # pylint: disable=protected-access simulation_json = asset._webapi.get( @@ -87,9 +74,14 @@ def _get_simulation_json(cls, asset: AssetBase) -> dict: @property def info(self) -> AssetMetaBaseModel: - """Return the metadata of the resource""" + """Return the metadata of the asset""" return self._webapi.info + @property + def entity_info(self): + """Return the entity info associated with the asset (copy to prevent unintentional overwrites)""" + return self._entity_info_class.model_validate(self._entity_info.model_dump()) + @classmethod def _interface(cls): return cls._interface_class @@ -151,20 +143,13 @@ def from_file( length_unit=length_unit, ) - def _inject_entity_info_to_params(self, params): - """Inject the length unit into the SimulationParams""" - # Add used cylinder, box, point and slice entities to the entityInfo. - # pylint: disable=protected-access - registry: EntityRegistry = params._get_used_entity_registry() - old_draft_entities = self._entity_info.draft_entities - # Step 1: Update old ones: - for _, old_entity in enumerate(old_draft_entities): - try: - _ = registry.find_by_naming_pattern(old_entity.name) - except ValueError: # old_entity did not apperar in params. - continue + def wait(self, timeout_minutes=60): + """Wait until the Asset finishes processing, refresh periodically""" - # pylint: disable=protected-access - with _model_attribute_unlock(params.private_attribute_asset_cache, "project_entity_info"): - params.private_attribute_asset_cache.project_entity_info = self._entity_info - return params + start_time = time.time() + while self._webapi.status.is_final() is False: + if time.time() - start_time > timeout_minutes * 60: + raise TimeoutError( + "Timeout: Process did not finish within the specified timeout period" + ) + time.sleep(2) diff --git a/flow360/component/utils.py b/flow360/component/utils.py index 3f76797d0..c56ab4675 100644 --- a/flow360/component/utils.py +++ b/flow360/component/utils.py @@ -2,11 +2,13 @@ Utility functions """ +import itertools import os import re from enum import Enum from functools import wraps from tempfile import NamedTemporaryFile +from typing import Generic, Iterable, Literal, Protocol, TypeVar import zstandard as zstd @@ -531,6 +533,29 @@ def is_valid_surface_mesh(self): def is_valid_volume_mesh(self): return self.format in [MeshFileFormat.UGRID, MeshFileFormat.CGNS] + # pylint: disable=missing-function-docstring + @staticmethod + def all_patterns(mesh_type: Literal["surface", "volume"]): + endian_format = [el.ext() for el in UGRIDEndianness] + mesh_format = [MeshFileFormat.UGRID.ext()] + + prod = itertools.product(endian_format, mesh_format) + + mesh_format = [endianness + file for (endianness, file) in prod] + + allowed = [] + if mesh_type == "surface": + allowed = [MeshFileFormat.UGRID, MeshFileFormat.CGNS, MeshFileFormat.STL] + elif mesh_type == "volume": + allowed = [MeshFileFormat.UGRID, MeshFileFormat.CGNS] + + mesh_format = mesh_format + [el.ext() for el in allowed] + compression = [el.ext() for el in CompressionFormat] + + prod = itertools.product(mesh_format, compression) + + return [file + compression for (file, compression) in prod] + def storage_size_formatter(size_in_bytes): """ @@ -553,3 +578,107 @@ def storage_size_formatter(size_in_bytes): if size_in_bytes < 1024**3: return f"{size_in_bytes / (1024 ** 2):.2f} MB" return f"{size_in_bytes / (1024 ** 3):.2f} GB" + + +# pylint: disable=too-few-public-methods +class HasId(Protocol): + """ + Protocol for objects that have an `id` attribute. + + Attributes + ---------- + id : str + Unique identifier for the asset. + """ + + id: str + + +AssetT = TypeVar("AssetT", bound=HasId) + + +class ProjectAssetCache(Generic[AssetT]): + """ + A cache to manage project assets with a unique ID system. + + Attributes + ---------- + current_asset_id : str, optional + The ID of the currently set asset. + asset_cache : dict of str to AssetT + Dictionary storing assets with their IDs as keys. + """ + + current_asset_id: str = None + asset_cache: dict[str, AssetT] = {} + + def get_asset(self, asset_id: str = None) -> AssetT: + """ + Retrieve an asset from the cache by ID. + + Parameters + ---------- + asset_id : str, optional + The ID of the asset to retrieve. If None, retrieves the asset with `current_id`. + + Returns + ------- + AssetT + The asset associated with the specified `asset_id`. + + Raises + ------ + Flow360ValueError + If the cache is empty or if the asset is not found. + """ + if not self.asset_cache: + raise Flow360ValueError("Cache is empty, no assets are available") + + asset = self.asset_cache.get(self.current_asset_id if not asset_id else asset_id) + + if not asset: + raise Flow360ValueError(f"{asset_id} is not available in the project.") + + return asset + + def get_ids(self) -> Iterable[str]: + """ + Retrieve all asset IDs in the cache. + + Returns + ------- + Iterable[str] + An iterable of asset IDs. + """ + return list(self.asset_cache.keys()) + + def add_asset(self, asset: AssetT): + """ + Add an asset to the cache. + + Parameters + ---------- + asset : AssetT + The asset to add. Must have a unique `id` attribute. + """ + self.asset_cache[asset.id] = asset + self.current_asset_id = asset.id + + def set_id(self, asset_id: str): + """ + Set the current ID to the given `asset_id`, if it exists in the cache. + + Parameters + ---------- + asset_id : str + The ID to set as the current ID. + + Raises + ------ + Flow360ValueError + If the specified `asset_id` does not exist in the cache. + """ + if asset_id not in self.asset_cache: + raise Flow360ValueError(f"{asset_id} is not available in the project.") + + self.current_asset_id = asset_id diff --git a/flow360/version.py b/flow360/version.py index e64a89d27..9d312c4ec 100644 --- a/flow360/version.py +++ b/flow360/version.py @@ -3,3 +3,4 @@ """ __version__ = "24.11.0" +__solver_version__ = "release-24.11" diff --git a/tests/simulation/service/test_integration_metadata.py b/tests/simulation/service/test_integration_metadata.py index 5bb7de1f7..9e924b74a 100644 --- a/tests/simulation/service/test_integration_metadata.py +++ b/tests/simulation/service/test_integration_metadata.py @@ -92,7 +92,7 @@ def test_update_zone_info_from_volume_mesh(get_volume_mesh_metadata): ], ) params._update_param_with_actual_volume_mesh_meta(get_volume_mesh_metadata) - my_reg = params._get_used_entity_registry() + my_reg = params.used_entity_registry assert isinstance( my_reg.find_single_entity_by_name("rotating_zone"), Cylinder, diff --git a/tests/test_case_webapi.py b/tests/test_case_webapi.py index b7acda868..f2a0f88ab 100644 --- a/tests/test_case_webapi.py +++ b/tests/test_case_webapi.py @@ -11,10 +11,10 @@ def test_case(mock_id, mock_response): case = Case(id=mock_id) log.info(f"{case.info}") log.info(f"{case.params.json()}") - log.info(f"case finished: {case.is_finished()}") + log.info(f"case finished: {case.status.is_final()}") log.info(f"case parent (parent={case.info.parent_id}): {case.has_parent()}") - assert case.is_finished() + assert case.status.is_final() assert case.is_steady() assert not case.has_actuator_disks() assert not case.has_bet_disks()