From 29024e4ce5d6e8635c579c89248c37d45134cc9c Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 24 Oct 2024 02:01:12 +0200 Subject: [PATCH 01/12] Added initial version of the Project class interface --- examples/project_interface_usage.py | 23 +++ flow360/component/project.py | 280 ++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 examples/project_interface_usage.py create mode 100644 flow360/component/project.py diff --git a/examples/project_interface_usage.py b/examples/project_interface_usage.py new file mode 100644 index 000000000..97767c5a8 --- /dev/null +++ b/examples/project_interface_usage.py @@ -0,0 +1,23 @@ +from flow360.component.project import Project +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.environment import dev +from flow360.component.simulation.unit_system import SI_unit_system + +dev.active() + +project = Project.from_cloud("prj-9c73a84b-6938-45c2-a8a3-5e3ffb2291b0") + +# This could be nicer... +simulation_json = project.get_simulation_json() +with SI_unit_system: + params = SimulationParams(**simulation_json) + +geometry = project.geometry +geometry.show_available_groupings(verbose_mode=True) +geometry.group_faces_by_tag("faceId") + +project.set_default_params(params) + +project.run_surface_mesher() +project.run_volume_mesher() +project.run_case() \ No newline at end of file diff --git a/flow360/component/project.py b/flow360/component/project.py new file mode 100644 index 000000000..e1acd93bc --- /dev/null +++ b/flow360/component/project.py @@ -0,0 +1,280 @@ +import json +from typing import Optional + +import pydantic as pd + +from enum import Enum + +from flow360 import SurfaceMesh, Case +from flow360.cloud.requests import LengthUnitType +from flow360.cloud.rest_api import RestApi +from flow360.component.geometry import Geometry +from flow360.component.interfaces import ProjectInterface, GeometryInterface, VolumeMeshInterfaceV2 +from flow360.component.simulation import cloud +from flow360.component.utils import SUPPORTED_GEOMETRY_FILE_PATTERNS, match_file_pattern, MeshNameParser +from flow360.component.volume_mesh import VolumeMesh +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.exceptions import Flow360ValueError, Flow360FileError, Flow360WebError + + +class RootType(Enum): + GEOMETRY = "Geometry" + VOLUME_MESH = "Volume mesh" + # SURFACE_MESH = "Surface mesh" - supported in a future iteration + + +class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): + 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") + + +class Project(pd.BaseModel): + info: ProjectMeta = pd.Field(None) + + # Right now the cached assets are all related to the + # root asset - we will have full project tree traversal + # in a future iteration. + _geometry: Optional[Geometry] = pd.PrivateAttr(None) + _volume_mesh: Optional[VolumeMesh] = pd.PrivateAttr(None) + _surface_mesh: Optional[SurfaceMesh] = pd.PrivateAttr(None) + _case: Optional[Case] = pd.PrivateAttr(None) + + _params: Optional[SimulationParams] = pd.PrivateAttr(None) + + @property + def geometry(self) -> Geometry: + """ + Getter for the current project's geometry asset + + Returns: Geometry asset if present, otherwise raises Flow360ValueError + """ + if not self._geometry: + raise Flow360ValueError("Geometry asset is not available for this project") + + return self._geometry + + @property + def surface_mesh(self) -> SurfaceMesh: + """ + Getter for the current project's surface mesh asset + + Returns: Surface mesh asset if present, otherwise raises Flow360ValueError + """ + if not self._surface_mesh: + raise Flow360ValueError("Surface mesh asset is not available for this project") + + return self._surface_mesh + + @property + def volume_mesh(self) -> VolumeMesh: + """ + Getter for the current project's volume mesh asset + + Returns: Volume mesh asset if present, otherwise raises Flow360ValueError + """ + if not self._volume_mesh: + raise Flow360ValueError("Volume mesh asset is not available for this project") + + return self._volume_mesh + + @property + def case(self) -> Case: + """ + Getter for the current project's case asset + + Returns: Case asset if present, otherwise raises Flow360ValueError + """ + if not self._case: + raise Flow360ValueError("Case asset is not available for this project") + + return self._case + + @classmethod + def _detect_root_type(cls, file): + if match_file_pattern(SUPPORTED_GEOMETRY_FILE_PATTERNS, file): + return RootType.GEOMETRY + else: + 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.") + + @classmethod + def from_file( + cls, + file: str = None, + name: str = None, + solver_version: str = None, + length_unit: LengthUnitType = "m" + ): + """ + Populates project data from a file + + Args: + name (): Name of the new project + file (): + solver_version (): + length_unit (): + + Returns: + + """ + + root_asset = None + + root_type = cls._detect_root_type(file) + + match root_type: + case RootType.GEOMETRY: + # Create a draft geometry asset and submit + draft = Geometry.from_file( + file, + name, + solver_version, + length_unit + ) + root_asset = draft.submit() + case RootType.VOLUME_MESH: + # Create a draft volume mesh asset and submit + draft = VolumeMesh.from_file( + file, + name, + solver_version, + ) + 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(info=ProjectMeta(**info)) + + match root_type: + case RootType.GEOMETRY: + project._geometry = root_asset + case RootType.VOLUME_MESH: + project._volume_mesh = root_asset + + return project + + @classmethod + def from_cloud(cls, project_id: str): + """Load project from cloud, only load the root asset for now""" + 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}") + + project = Project(info=ProjectMeta(**info)) + + root_type = project.info.root_item_type + + match root_type: + case RootType.GEOMETRY: + root_asset = Geometry.from_cloud(project.info.root_item_id) + project._geometry = root_asset + case RootType.VOLUME_MESH: + root_asset = VolumeMesh.from_cloud(project.info.root_item_id) + project._volume_mesh = root_asset + + return project + + def _check_initialized(self): + if not self.info: + raise Flow360ValueError("Project is not initialized - use Project.from_file or Project.from_cloud") + + def set_default_params(self, params: SimulationParams): + self._params = params + + def get_simulation_json(self): + """ + Get default simulation JSON for the project based on the root asset type + """ + self._check_initialized() + + root_type = self.info.root_item_type + root_id = self.info.root_item_id + + if not root_type or not root_id: + raise Flow360ValueError("Root item type or ID is missing from project metadata") + + root_api = None + + match root_type: + case RootType.GEOMETRY: + root_api = RestApi(GeometryInterface.endpoint, id=root_id) + case RootType.VOLUME_MESH: + root_api = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_id) + + resp = root_api.get(method="simulation/file", params={"type": "simulation"}) + + if not isinstance(resp, dict) or "simulationJson" not in resp: + raise Flow360WebError("Root item type or ID is missing from project metadata") + + simulation_json = json.loads(resp["simulationJson"]) + + return simulation_json + + def run_surface_mesher(self, params: SimulationParams = None, draft_name: str = "Surface Mesh"): + """Run surface mesher with the provided params or defaults""" + self._check_initialized() + + if self.info.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError("Surface mesher can only be ran by projects with a geometry root asset") + if self._geometry is None: + raise Flow360ValueError("This project object was not initialized using from_cloud or from_file") + # We should remove usages of cloud static functions here + self._surface_mesh = cloud.generate_surface_mesh( + self._geometry, + params=params if params else self._params, + draft_name=draft_name, + async_mode=False + ) + + def run_volume_mesher(self, params: SimulationParams = None, draft_name: str = "Volume Mesh"): + """Run volume mesher with the provided params or defaults""" + self._check_initialized() + + if self.info.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError("Volume mesher can only be ran by projects with a geometry root asset") + if self._surface_mesh is None: + raise Flow360ValueError("This project object is missing a surface mesh, fix by running run_surface_mesher") + # We should remove usages of cloud static functions here + self._volume_mesh = cloud.generate_volume_mesh( + self._surface_mesh, + params=params if params else self._params, + draft_name=draft_name, + async_mode=False + ) + + def run_case(self, params: SimulationParams = None, draft_name: str = "Case"): + """Run project with the provided params""" + self._check_initialized() + + if self._volume_mesh is None: + raise Flow360ValueError("This project object is missing a volume mesh, fix by running run_volume_mesher") + # We should remove usages of cloud static functions here + self._case = cloud.generate_volume_mesh( + self._volume_mesh, + params=params if params else self._params, + draft_name=draft_name, + async_mode=False + ) From 4f82beb34ddbd77e385e0b01ec1254f76efeaa0c Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 24 Oct 2024 14:27:47 +0200 Subject: [PATCH 02/12] Added examples, remove usages of cloud static functions --- examples/project_from_cloud_geometry.py | 26 ++++ examples/project_from_cloud_volume_mesh.py | 28 ++++ examples/project_from_file_geometry.py | 18 +++ examples/project_from_file_volume_mesh.py | 44 ++++++ examples/project_interface_usage.py | 23 --- flow360/component/case.py | 17 --- flow360/component/project.py | 135 ++++++++++++----- flow360/component/resource_base.py | 12 ++ flow360/component/simulation/cloud.py | 141 ------------------ .../component/simulation/simulation_params.py | 17 ++- .../component/simulation/web/asset_base.py | 38 +---- 11 files changed, 252 insertions(+), 247 deletions(-) create mode 100644 examples/project_from_cloud_geometry.py create mode 100644 examples/project_from_cloud_volume_mesh.py create mode 100644 examples/project_from_file_geometry.py create mode 100644 examples/project_from_file_volume_mesh.py delete mode 100644 examples/project_interface_usage.py delete mode 100644 flow360/component/simulation/cloud.py diff --git a/examples/project_from_cloud_geometry.py b/examples/project_from_cloud_geometry.py new file mode 100644 index 000000000..2e7818451 --- /dev/null +++ b/examples/project_from_cloud_geometry.py @@ -0,0 +1,26 @@ +from flow360.component.project import Project +from flow360.component.simulation.meshing_param.params import MeshingParams, MeshingDefaults +from flow360.component.simulation.meshing_param.volume_params import AutomatedFarfield +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.environment import dev +from flow360.component.simulation.unit_system import SI_unit_system + +dev.active() + +project = Project.from_cloud("prj-5159c384-fc07-4bd1-b30f-e4ea0dcafa2d") +print(project.get_simulation_json()) + +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()], + ), + ) + +project.set_default_params(params) +project.run_case() diff --git a/examples/project_from_cloud_volume_mesh.py b/examples/project_from_cloud_volume_mesh.py new file mode 100644 index 000000000..b5a3928cc --- /dev/null +++ b/examples/project_from_cloud_volume_mesh.py @@ -0,0 +1,28 @@ +from flow360.component.project import Project +import flow360.component.simulation.units as u +from flow360.component.simulation.models.surface_models import Freestream, Wall +from flow360.component.simulation.models.volume_models import Fluid +from flow360.component.simulation.operating_condition.operating_condition import AerospaceCondition +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.environment import dev +from flow360.component.simulation.unit_system import SI_unit_system + +dev.active() + +project = Project.from_cloud("prj-e8c6c7eb-c18b-4c15-bac8-edf5aaf9b155") +print(project.get_simulation_json()) + +volume_mesh = project.volume_mesh + +with SI_unit_system: + params = SimulationParams( + operating_condition=AerospaceCondition(velocity_magnitude=100 * u.m / u.s), + models=[ + Fluid(), + Wall(entities=[volume_mesh["fluid/wall"]]), + Freestream(entities=[volume_mesh["fluid/farfield"]]), + ], + ) + +project.set_default_params(params) +project.run_case() diff --git a/examples/project_from_file_geometry.py b/examples/project_from_file_geometry.py new file mode 100644 index 000000000..ab248c669 --- /dev/null +++ b/examples/project_from_file_geometry.py @@ -0,0 +1,18 @@ +import flow360 as fl +from flow360.component.project import Project +from flow360.examples import Airplane + +fl.Env.dev.active() + +Airplane.get_files() + +project = Project.from_file( + Airplane.geometry, + name="airplane-geometry-python-upload", + solver_version="workbench-24.9.3", + tags=["python"], +) + +geometry = project.geometry + +print(geometry) diff --git a/examples/project_from_file_volume_mesh.py b/examples/project_from_file_volume_mesh.py new file mode 100644 index 000000000..6b0ba2311 --- /dev/null +++ b/examples/project_from_file_volume_mesh.py @@ -0,0 +1,44 @@ +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.models.volume_models import Fluid +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", + solver_version="workbench-24.9.3", + tags=["python"], +) + +volume_mesh = project.volume_mesh + +with SI_unit_system: + params = SimulationParams( + operating_condition=AerospaceCondition(velocity_magnitude=100 * u.m / u.s), + models=[ + Fluid(), + Wall(entities=[volume_mesh["1"]]), + Freestream(entities=[volume_mesh["3"]]), + SymmetryPlane(entities=[volume_mesh["2"]]), + ], + ) + +project.set_default_params(params) +project.run_case() diff --git a/examples/project_interface_usage.py b/examples/project_interface_usage.py deleted file mode 100644 index 97767c5a8..000000000 --- a/examples/project_interface_usage.py +++ /dev/null @@ -1,23 +0,0 @@ -from flow360.component.project import Project -from flow360.component.simulation.simulation_params import SimulationParams -from flow360.environment import dev -from flow360.component.simulation.unit_system import SI_unit_system - -dev.active() - -project = Project.from_cloud("prj-9c73a84b-6938-45c2-a8a3-5e3ffb2291b0") - -# This could be nicer... -simulation_json = project.get_simulation_json() -with SI_unit_system: - params = SimulationParams(**simulation_json) - -geometry = project.geometry -geometry.show_available_groupings(verbose_mode=True) -geometry.group_faces_by_tag("faceId") - -project.set_default_params(params) - -project.run_surface_mesher() -project.run_volume_mesher() -project.run_case() \ No newline at end of file diff --git a/flow360/component/case.py b/flow360/component/case.py index d104f6b40..bd2a533d6 100644 --- a/flow360/component/case.py +++ b/flow360/component/case.py @@ -524,12 +524,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 +620,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/project.py b/flow360/component/project.py index e1acd93bc..c7e212e1f 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1,5 +1,6 @@ import json -from typing import Optional +import time +from typing import Optional, Union, List import pydantic as pd @@ -10,17 +11,25 @@ from flow360.cloud.rest_api import RestApi from flow360.component.geometry import Geometry from flow360.component.interfaces import ProjectInterface, GeometryInterface, VolumeMeshInterfaceV2 -from flow360.component.simulation import cloud +from flow360.component.resource_base import Flow360Resource +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, match_file_pattern, MeshNameParser -from flow360.component.volume_mesh import VolumeMesh +from flow360.component.volume_mesh import VolumeMeshV2 from flow360.component.simulation.simulation_params import SimulationParams from flow360.exceptions import Flow360ValueError, Flow360FileError, Flow360WebError +# This is used before all resources are moved to V2 API as AssetBase subclasses +AssetOrResource = Union[type[AssetBase], type[Flow360Resource]] +RootAsset = Union[Geometry, VolumeMeshV2] + class RootType(Enum): GEOMETRY = "Geometry" - VOLUME_MESH = "Volume mesh" - # SURFACE_MESH = "Surface mesh" - supported in a future iteration + VOLUME_MESH = "VolumeMesh" + # SURFACE_MESH = "SurfaceMesh" - supported in a future iteration class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): @@ -33,12 +42,13 @@ class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): class Project(pd.BaseModel): info: ProjectMeta = pd.Field(None) + solver_version: str = pd.Field(None, frozen=True) # Right now the cached assets are all related to the # root asset - we will have full project tree traversal # in a future iteration. _geometry: Optional[Geometry] = pd.PrivateAttr(None) - _volume_mesh: Optional[VolumeMesh] = pd.PrivateAttr(None) + _volume_mesh: Optional[VolumeMeshV2] = pd.PrivateAttr(None) _surface_mesh: Optional[SurfaceMesh] = pd.PrivateAttr(None) _case: Optional[Case] = pd.PrivateAttr(None) @@ -69,7 +79,7 @@ def surface_mesh(self) -> SurfaceMesh: return self._surface_mesh @property - def volume_mesh(self) -> VolumeMesh: + def volume_mesh(self) -> VolumeMeshV2: """ Getter for the current project's volume mesh asset @@ -112,7 +122,8 @@ def from_file( file: str = None, name: str = None, solver_version: str = None, - length_unit: LengthUnitType = "m" + length_unit: LengthUnitType = "m", + tags: List[str] = None, ): """ Populates project data from a file @@ -122,6 +133,7 @@ def from_file( file (): solver_version (): length_unit (): + tags (): Returns: @@ -138,15 +150,18 @@ def from_file( file, name, solver_version, - length_unit + length_unit, + tags ) root_asset = draft.submit() case RootType.VOLUME_MESH: # Create a draft volume mesh asset and submit - draft = VolumeMesh.from_file( + draft = VolumeMeshV2.from_file( file, name, solver_version, + length_unit, + tags ) root_asset = draft.submit() @@ -161,7 +176,7 @@ def from_file( if not info: raise Flow360ValueError(f"Couldn't retrieve project info for {project_id}") - project = Project(info=ProjectMeta(**info)) + project = Project(info=ProjectMeta(**info), solver_version=root_asset.solver_version) match root_type: case RootType.GEOMETRY: @@ -183,28 +198,38 @@ def from_cloud(cls, project_id: str): if not info: raise Flow360WebError(f"Couldn't retrieve project info for {project_id}") - project = Project(info=ProjectMeta(**info)) + meta = ProjectMeta(**info) + + root_asset = None + root_type = meta.root_item_type + + match root_type: + case RootType.GEOMETRY: + root_asset = Geometry.from_cloud(meta.root_item_id) + case RootType.VOLUME_MESH: + root_asset = VolumeMeshV2.from_cloud(meta.root_item_id) + + project = Project(info=meta, solver_version=root_asset.solver_version) - root_type = project.info.root_item_type + if not root_asset: + raise Flow360ValueError(f"Couldn't retrieve root asset for {project_id}") match root_type: case RootType.GEOMETRY: - root_asset = Geometry.from_cloud(project.info.root_item_id) project._geometry = root_asset case RootType.VOLUME_MESH: - root_asset = VolumeMesh.from_cloud(project.info.root_item_id) project._volume_mesh = root_asset return project def _check_initialized(self): - if not self.info: + if not self.info or not self.solver_version: raise Flow360ValueError("Project is not initialized - use Project.from_file or Project.from_cloud") def set_default_params(self, params: SimulationParams): self._params = params - def get_simulation_json(self): + def get_simulation_json(self) -> dict: """ Get default simulation JSON for the project based on the root asset type """ @@ -233,20 +258,67 @@ def get_simulation_json(self): return simulation_json + def _run(self, params: SimulationParams, target: AssetOrResource, draft_name: str = None, fork: bool = False): + defaults = self.get_simulation_json() + + cache_key = "private_attribute_asset_cache" + length_key = "project_length_unit" + + if cache_key not in defaults or 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 = None + root_type = self.info.root_item_type + + match root_type: + case RootType.GEOMETRY: + root_asset = self._geometry + case RootType.VOLUME_MESH: + root_asset = self._volume_mesh + + if not root_asset: + raise Flow360ValueError(f"Root asset is not initialized") + + draft = Draft.create( + name=draft_name, + project_id=self.info.id, + source_item_id=self.info.root_item_id, + source_item_type=root_type.value, + solver_version=self.solver_version, + fork_case=fork, + ).submit() + + draft.update_simulation_params(params) + destination_id = draft.run_up_to_target_asset(target) + + RestApi(ProjectInterface.endpoint, id=self.info.id).patch( + json={ + "lastOpenItemId": destination_id, + "lastOpenItemType": target.__name__, + } + ) + + destination_obj = target.from_cloud(destination_id) + destination_obj.wait() + + return destination_obj + def run_surface_mesher(self, params: SimulationParams = None, draft_name: str = "Surface Mesh"): """Run surface mesher with the provided params or defaults""" self._check_initialized() if self.info.root_item_type is not RootType.GEOMETRY: raise Flow360ValueError("Surface mesher can only be ran by projects with a geometry root asset") - if self._geometry is None: - raise Flow360ValueError("This project object was not initialized using from_cloud or from_file") - # We should remove usages of cloud static functions here - self._surface_mesh = cloud.generate_surface_mesh( - self._geometry, + + self._surface_mesh = self._run( params=params if params else self._params, + target=SurfaceMesh, draft_name=draft_name, - async_mode=False ) def run_volume_mesher(self, params: SimulationParams = None, draft_name: str = "Volume Mesh"): @@ -255,26 +327,19 @@ def run_volume_mesher(self, params: SimulationParams = None, draft_name: str = " if self.info.root_item_type is not RootType.GEOMETRY: raise Flow360ValueError("Volume mesher can only be ran by projects with a geometry root asset") - if self._surface_mesh is None: - raise Flow360ValueError("This project object is missing a surface mesh, fix by running run_surface_mesher") - # We should remove usages of cloud static functions here - self._volume_mesh = cloud.generate_volume_mesh( - self._surface_mesh, + + self._volume_mesh = self._run( params=params if params else self._params, + target=VolumeMeshV2, draft_name=draft_name, - async_mode=False ) def run_case(self, params: SimulationParams = None, draft_name: str = "Case"): """Run project with the provided params""" self._check_initialized() - if self._volume_mesh is None: - raise Flow360ValueError("This project object is missing a volume mesh, fix by running run_volume_mesher") - # We should remove usages of cloud static functions here - self._case = cloud.generate_volume_mesh( - self._volume_mesh, + self._case = self._run( params=params if params else self._params, + target=Case, draft_name=draft_name, - async_mode=False ) 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/simulation_params.py b/flow360/component/simulation/simulation_params.py index a86b7c94b..013099d43 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -56,6 +56,7 @@ ) from flow360.exceptions import Flow360ConfigurationError, Flow360RuntimeError from flow360.version import __version__ +from .utils import _model_attribute_unlock from .validation.validation_context import SURFACE_MESH, CaseField, ContextField @@ -308,7 +309,21 @@ def _get_used_entity_registry(self) -> EntityRegistry: registry = self._update_entity_private_attrs(registry) return registry - ##:: Internal Util functions + def _inject_entity_info_to_params(self, entity_info): + """Inject the length unit into the SimulationParams""" + # Add used cylinder, box, point and slice entities to the entityInfo. + registry: EntityRegistry = self._get_used_entity_registry() + old_draft_entities = entity_info.draft_entities + for _, old_entity in enumerate(old_draft_entities): + try: + _ = registry.find_by_naming_pattern(old_entity.name) + except ValueError: # old_entity did not appear in params. + continue + + # pylint: disable=protected-access + with _model_attribute_unlock(self.private_attribute_asset_cache, "project_entity_info"): + self.private_attribute_asset_cache.project_entity_info = entity_info + 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. diff --git a/flow360/component/simulation/web/asset_base.py b/flow360/component/simulation/web/asset_base.py index da6d497b2..81c24dccf 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( @@ -151,20 +138,11 @@ 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) From 8ca3122d9a55027934381e9acf8d64f9d8f94d53 Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 24 Oct 2024 16:26:46 +0200 Subject: [PATCH 03/12] Entity info injection during _run() --- examples/project_from_file_geometry.py | 50 ++++++++++++++++--- flow360/component/project.py | 19 ++++++- .../component/simulation/simulation_params.py | 20 ++------ .../component/simulation/web/asset_base.py | 8 ++- .../service/test_integration_metadata.py | 2 +- 5 files changed, 70 insertions(+), 29 deletions(-) diff --git a/examples/project_from_file_geometry.py b/examples/project_from_file_geometry.py index ab248c669..1e027afde 100644 --- a/examples/project_from_file_geometry.py +++ b/examples/project_from_file_geometry.py @@ -1,18 +1,52 @@ 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.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.outputs.outputs import SurfaceOutput +from flow360.component.simulation.unit_system import SI_unit_system, u from flow360.examples import Airplane fl.Env.dev.active() -Airplane.get_files() +SOLVER_VERSION = "workbench-24.9.3" -project = Project.from_file( - Airplane.geometry, - name="airplane-geometry-python-upload", - solver_version="workbench-24.9.3", - tags=["python"], -) +project = Project.from_file(Airplane.geometry, name='Simple Airplane from Python', solver_version=SOLVER_VERSION) 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'])] + ) + +case = project.run_case(params=params, draft_name="Case of Simple Airplane from Python") -print(geometry) diff --git a/flow360/component/project.py b/flow360/component/project.py index c7e212e1f..2857f6ab2 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -12,6 +12,8 @@ from flow360.component.geometry import Geometry from flow360.component.interfaces import ProjectInterface, GeometryInterface, VolumeMeshInterfaceV2 from flow360.component.resource_base import Flow360Resource +from flow360.component.simulation.entity_info import GeometryEntityInfo, VolumeMeshEntityInfo +from flow360.component.simulation.framework.entity_registry import EntityRegistry 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 @@ -264,8 +266,9 @@ def _run(self, params: SimulationParams, target: AssetOrResource, draft_name: st cache_key = "private_attribute_asset_cache" length_key = "project_length_unit" - if cache_key not in defaults or length_key not in defaults[cache_key]: - raise Flow360ValueError("Simulation params do not contain default length unit info") + 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] @@ -293,6 +296,18 @@ def _run(self, params: SimulationParams, target: AssetOrResource, draft_name: st fork_case=fork, ).submit() + entity_info = root_asset.entity_info + registry = params.used_entity_registry + old_draft_entities = entity_info.draft_entities + for _, old_entity in enumerate(old_draft_entities): + try: + registry.find_by_naming_pattern(old_entity.name) + except ValueError: + continue + + 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) diff --git a/flow360/component/simulation/simulation_params.py b/flow360/component/simulation/simulation_params.py index 013099d43..b8fad6432 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -299,7 +299,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. @@ -309,21 +310,6 @@ def _get_used_entity_registry(self) -> EntityRegistry: registry = self._update_entity_private_attrs(registry) return registry - def _inject_entity_info_to_params(self, entity_info): - """Inject the length unit into the SimulationParams""" - # Add used cylinder, box, point and slice entities to the entityInfo. - registry: EntityRegistry = self._get_used_entity_registry() - old_draft_entities = entity_info.draft_entities - for _, old_entity in enumerate(old_draft_entities): - try: - _ = registry.find_by_naming_pattern(old_entity.name) - except ValueError: # old_entity did not appear in params. - continue - - # pylint: disable=protected-access - with _model_attribute_unlock(self.private_attribute_asset_cache, "project_entity_info"): - self.private_attribute_asset_cache.project_entity_info = entity_info - 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. @@ -332,7 +318,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/web/asset_base.py b/flow360/component/simulation/web/asset_base.py index 81c24dccf..b430ce48f 100644 --- a/flow360/component/simulation/web/asset_base.py +++ b/flow360/component/simulation/web/asset_base.py @@ -2,6 +2,7 @@ from __future__ import annotations +import abc import json import time from abc import ABCMeta @@ -74,9 +75,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 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, From 266df20a1c1613af6b1cabfcc9d41e0a39a558f4 Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 24 Oct 2024 18:15:38 +0200 Subject: [PATCH 04/12] Style fixes --- examples/project_from_cloud_geometry.py | 7 +- examples/project_from_cloud_volume_mesh.py | 8 +- examples/project_from_file_geometry.py | 12 +- examples/project_from_file_volume_mesh.py | 2 - flow360/component/case.py | 1 - flow360/component/geometry.py | 6 +- flow360/component/project.py | 228 ++++++++++-------- .../framework/multi_constructor_model_base.py | 6 +- .../simulation/framework/param_utils.py | 6 +- flow360/component/simulation/primitives.py | 8 +- flow360/component/simulation/services.py | 4 +- .../component/simulation/simulation_params.py | 1 - flow360/component/simulation/utils.py | 5 +- .../component/simulation/web/asset_base.py | 5 +- 14 files changed, 163 insertions(+), 136 deletions(-) diff --git a/examples/project_from_cloud_geometry.py b/examples/project_from_cloud_geometry.py index 2e7818451..d6369701c 100644 --- a/examples/project_from_cloud_geometry.py +++ b/examples/project_from_cloud_geometry.py @@ -1,9 +1,12 @@ from flow360.component.project import Project -from flow360.component.simulation.meshing_param.params import MeshingParams, MeshingDefaults +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.environment import dev from flow360.component.simulation.unit_system import SI_unit_system +from flow360.environment import dev dev.active() diff --git a/examples/project_from_cloud_volume_mesh.py b/examples/project_from_cloud_volume_mesh.py index b5a3928cc..8dd9d1726 100644 --- a/examples/project_from_cloud_volume_mesh.py +++ b/examples/project_from_cloud_volume_mesh.py @@ -1,11 +1,13 @@ -from flow360.component.project import Project import flow360.component.simulation.units as u +from flow360.component.project import Project from flow360.component.simulation.models.surface_models import Freestream, Wall from flow360.component.simulation.models.volume_models import Fluid -from flow360.component.simulation.operating_condition.operating_condition import AerospaceCondition +from flow360.component.simulation.operating_condition.operating_condition import ( + AerospaceCondition, +) from flow360.component.simulation.simulation_params import SimulationParams -from flow360.environment import dev from flow360.component.simulation.unit_system import SI_unit_system +from flow360.environment import dev dev.active() diff --git a/examples/project_from_file_geometry.py b/examples/project_from_file_geometry.py index 1e027afde..686b4494d 100644 --- a/examples/project_from_file_geometry.py +++ b/examples/project_from_file_geometry.py @@ -1,6 +1,5 @@ import flow360 as fl from flow360.component.project import Project - from flow360.component.simulation.meshing_param.params import ( MeshingDefaults, MeshingParams, @@ -10,10 +9,10 @@ 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.outputs.outputs import SurfaceOutput from flow360.component.simulation.unit_system import SI_unit_system, u from flow360.examples import Airplane @@ -21,7 +20,9 @@ SOLVER_VERSION = "workbench-24.9.3" -project = Project.from_file(Airplane.geometry, name='Simple Airplane from Python', solver_version=SOLVER_VERSION) +project = Project.from_file( + Airplane.geometry, name="airplane-geometry-python-upload", solver_version=SOLVER_VERSION +) geometry = project.geometry geometry.show_available_groupings(verbose_mode=True) @@ -45,8 +46,9 @@ ), Freestream(surfaces=[AutomatedFarfield().farfield], name="Freestream"), ], - outputs=[SurfaceOutput(surfaces=geometry["*"], output_fields=['Cp', 'Cf', 'yPlus', 'CfVec'])] + outputs=[ + SurfaceOutput(surfaces=geometry["*"], output_fields=["Cp", "Cf", "yPlus", "CfVec"]) + ], ) case = project.run_case(params=params, draft_name="Case of Simple Airplane from Python") - diff --git a/examples/project_from_file_volume_mesh.py b/examples/project_from_file_volume_mesh.py index 6b0ba2311..52e468545 100644 --- a/examples/project_from_file_volume_mesh.py +++ b/examples/project_from_file_volume_mesh.py @@ -1,6 +1,5 @@ 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, @@ -11,7 +10,6 @@ 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 diff --git a/flow360/component/case.py b/flow360/component/case.py index bd2a533d6..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 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 index 2857f6ab2..1d0cd6a5f 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1,27 +1,36 @@ +"""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 -import time -from typing import Optional, Union, List +from enum import Enum +from typing import List, Optional, Union import pydantic as pd -from enum import Enum - -from flow360 import SurfaceMesh, Case +from flow360 import Case, SurfaceMesh from flow360.cloud.requests import LengthUnitType from flow360.cloud.rest_api import RestApi from flow360.component.geometry import Geometry -from flow360.component.interfaces import ProjectInterface, GeometryInterface, VolumeMeshInterfaceV2 +from flow360.component.interfaces import ( + GeometryInterface, + ProjectInterface, + VolumeMeshInterfaceV2, +) from flow360.component.resource_base import Flow360Resource -from flow360.component.simulation.entity_info import GeometryEntityInfo, VolumeMeshEntityInfo -from flow360.component.simulation.framework.entity_registry import EntityRegistry +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.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, match_file_pattern, MeshNameParser +from flow360.component.utils import ( + SUPPORTED_GEOMETRY_FILE_PATTERNS, + MeshNameParser, + match_file_pattern, +) from flow360.component.volume_mesh import VolumeMeshV2 -from flow360.component.simulation.simulation_params import SimulationParams -from flow360.exceptions import Flow360ValueError, Flow360FileError, Flow360WebError +from flow360.exceptions import Flow360FileError, Flow360ValueError, Flow360WebError # This is used before all resources are moved to V2 API as AssetBase subclasses AssetOrResource = Union[type[AssetBase], type[Flow360Resource]] @@ -29,12 +38,20 @@ class RootType(Enum): + """ + Type of a root object of the project + """ + GEOMETRY = "Geometry" VOLUME_MESH = "VolumeMesh" # SURFACE_MESH = "SurfaceMesh" - supported in a future iteration class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): + """ + Metadata of the project + """ + user_id: str = pd.Field(alias="userId") id: str = pd.Field() name: str = pd.Field() @@ -43,8 +60,12 @@ class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): class Project(pd.BaseModel): - info: ProjectMeta = pd.Field(None) - solver_version: str = pd.Field(None, frozen=True) + """ + Project class containing the interface for creating and running simulations + """ + + metadata: ProjectMeta = pd.Field() + solver_version: str = pd.Field(frozen=True) # Right now the cached assets are all related to the # root asset - we will have full project tree traversal @@ -54,8 +75,6 @@ class Project(pd.BaseModel): _surface_mesh: Optional[SurfaceMesh] = pd.PrivateAttr(None) _case: Optional[Case] = pd.PrivateAttr(None) - _params: Optional[SimulationParams] = pd.PrivateAttr(None) - @property def geometry(self) -> Geometry: """ @@ -108,16 +127,19 @@ def case(self) -> Case: def _detect_root_type(cls, file): if match_file_pattern(SUPPORTED_GEOMETRY_FILE_PATTERNS, file): return RootType.GEOMETRY - else: - 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.") + 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." + ) + # pylint: disable=too-many-arguments @classmethod def from_file( cls, @@ -145,27 +167,14 @@ def from_file( root_type = cls._detect_root_type(file) - match root_type: - case RootType.GEOMETRY: - # Create a draft geometry asset and submit - draft = Geometry.from_file( - file, - name, - solver_version, - length_unit, - tags - ) - root_asset = draft.submit() - case RootType.VOLUME_MESH: - # Create a draft volume mesh asset and submit - draft = VolumeMeshV2.from_file( - file, - name, - solver_version, - length_unit, - tags - ) - root_asset = draft.submit() + if root_type == RootType.GEOMETRY: + # Create a draft geometry asset and submit + draft = Geometry.from_file(file, name, solver_version, length_unit, tags) + root_asset = draft.submit() + elif root_type == RootType.VOLUME_MESH: + # Create a draft volume mesh asset and submit + 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}") @@ -178,13 +187,12 @@ def from_file( if not info: raise Flow360ValueError(f"Couldn't retrieve project info for {project_id}") - project = Project(info=ProjectMeta(**info), solver_version=root_asset.solver_version) + project = Project(metadata=ProjectMeta(**info), solver_version=root_asset.solver_version) - match root_type: - case RootType.GEOMETRY: - project._geometry = root_asset - case RootType.VOLUME_MESH: - project._volume_mesh = root_asset + if root_type == RootType.GEOMETRY: + project._geometry = root_asset + elif root_type == RootType.VOLUME_MESH: + project._volume_mesh = root_asset return project @@ -205,31 +213,31 @@ def from_cloud(cls, project_id: str): root_asset = None root_type = meta.root_item_type - match root_type: - case RootType.GEOMETRY: - root_asset = Geometry.from_cloud(meta.root_item_id) - case RootType.VOLUME_MESH: - root_asset = VolumeMeshV2.from_cloud(meta.root_item_id) + 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) project = Project(info=meta, solver_version=root_asset.solver_version) if not root_asset: raise Flow360ValueError(f"Couldn't retrieve root asset for {project_id}") - match root_type: - case RootType.GEOMETRY: - project._geometry = root_asset - case RootType.VOLUME_MESH: - project._volume_mesh = root_asset + if root_type == RootType.GEOMETRY: + project._geometry = root_asset + elif root_type == RootType.VOLUME_MESH: + project._volume_mesh = root_asset return project def _check_initialized(self): - if not self.info or not self.solver_version: - raise Flow360ValueError("Project is not initialized - use Project.from_file or Project.from_cloud") - - def set_default_params(self, params: SimulationParams): - self._params = params + """ + Check if the Project instance has been initialized correctly + """ + if not self.metadata or not self.solver_version: + raise Flow360ValueError( + "Project is not initialized - use Project.from_file or Project.from_cloud" + ) def get_simulation_json(self) -> dict: """ @@ -237,19 +245,18 @@ def get_simulation_json(self) -> dict: """ self._check_initialized() - root_type = self.info.root_item_type - root_id = self.info.root_item_id + 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") root_api = None - match root_type: - case RootType.GEOMETRY: - root_api = RestApi(GeometryInterface.endpoint, id=root_id) - case RootType.VOLUME_MESH: - root_api = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_id) + if root_type == RootType.GEOMETRY: + root_api = RestApi(GeometryInterface.endpoint, id=root_id) + elif root_type == RootType.VOLUME_MESH: + root_api = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_id) resp = root_api.get(method="simulation/file", params={"type": "simulation"}) @@ -260,7 +267,15 @@ def get_simulation_json(self) -> dict: return simulation_json - def _run(self, params: SimulationParams, target: AssetOrResource, draft_name: str = None, fork: bool = False): + # pylint: disable=too-many-locals, too-many-arguments + def _run( + self, + params: SimulationParams, + target: AssetOrResource, + draft_name: str = None, + fork: bool = False, + run_async: bool = True, + ): defaults = self.get_simulation_json() cache_key = "private_attribute_asset_cache" @@ -272,25 +287,26 @@ def _run(self, params: SimulationParams, target: AssetOrResource, draft_name: st 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) + 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 = None - root_type = self.info.root_item_type + root_type = self.metadata.root_item_type - match root_type: - case RootType.GEOMETRY: - root_asset = self._geometry - case RootType.VOLUME_MESH: - root_asset = self._volume_mesh + if root_type == RootType.GEOMETRY: + root_asset = self._geometry + elif root_type == RootType.VOLUME_MESH: + root_asset = self._volume_mesh if not root_asset: - raise Flow360ValueError(f"Root asset is not initialized") + raise Flow360ValueError("Root asset is not initialized") draft = Draft.create( name=draft_name, - project_id=self.info.id, - source_item_id=self.info.root_item_id, + project_id=self.metadata.id, + source_item_id=self.metadata.root_item_id, source_item_type=root_type.value, solver_version=self.solver_version, fork_case=fork, @@ -305,13 +321,13 @@ def _run(self, params: SimulationParams, target: AssetOrResource, draft_name: st except ValueError: continue - with _model_attribute_unlock(params.private_attribute_asset_cache, "project_entity_info"): + 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) - RestApi(ProjectInterface.endpoint, id=self.info.id).patch( + RestApi(ProjectInterface.endpoint, id=self.metadata.id).patch( json={ "lastOpenItemId": destination_id, "lastOpenItemType": target.__name__, @@ -319,42 +335,46 @@ def _run(self, params: SimulationParams, target: AssetOrResource, draft_name: st ) destination_obj = target.from_cloud(destination_id) - destination_obj.wait() + + if not run_async: + destination_obj.wait() return destination_obj - def run_surface_mesher(self, params: SimulationParams = None, draft_name: str = "Surface Mesh"): + def run_surface_mesher( + self, params: SimulationParams, draft_name: str = "SurfaceMesh", run_async: bool = False + ): """Run surface mesher with the provided params or defaults""" self._check_initialized() - if self.info.root_item_type is not RootType.GEOMETRY: - raise Flow360ValueError("Surface mesher can only be ran by projects with a geometry root asset") + if self.metadata.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError( + "Surface mesher can only be ran by projects with a geometry root asset" + ) self._surface_mesh = self._run( - params=params if params else self._params, - target=SurfaceMesh, - draft_name=draft_name, + params=params, target=SurfaceMesh, draft_name=draft_name, run_async=run_async ) - def run_volume_mesher(self, params: SimulationParams = None, draft_name: str = "Volume Mesh"): + def run_volume_mesher( + self, params: SimulationParams, draft_name: str = "VolumeMesh", run_async: bool = False + ): """Run volume mesher with the provided params or defaults""" self._check_initialized() - if self.info.root_item_type is not RootType.GEOMETRY: - raise Flow360ValueError("Volume mesher can only be ran by projects with a geometry root asset") + if self.metadata.root_item_type is not RootType.GEOMETRY: + raise Flow360ValueError( + "Volume mesher can only be ran by projects with a geometry root asset" + ) self._volume_mesh = self._run( - params=params if params else self._params, - target=VolumeMeshV2, - draft_name=draft_name, + params=params, target=VolumeMeshV2, draft_name=draft_name, run_async=run_async ) - def run_case(self, params: SimulationParams = None, draft_name: str = "Case"): + def run_case(self, params: SimulationParams, draft_name: str = "Case", run_async: bool = False): """Run project with the provided params""" self._check_initialized() self._case = self._run( - params=params if params else self._params, - target=Case, - draft_name=draft_name, + params=params, target=Case, draft_name=draft_name, run_async=run_async ) 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 b8fad6432..99a088e08 100644 --- a/flow360/component/simulation/simulation_params.py +++ b/flow360/component/simulation/simulation_params.py @@ -56,7 +56,6 @@ ) from flow360.exceptions import Flow360ConfigurationError, Flow360RuntimeError from flow360.version import __version__ -from .utils import _model_attribute_unlock from .validation.validation_context import SURFACE_MESH, CaseField, ContextField 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 b430ce48f..96efd0944 100644 --- a/flow360/component/simulation/web/asset_base.py +++ b/flow360/component/simulation/web/asset_base.py @@ -2,7 +2,6 @@ from __future__ import annotations -import abc import json import time from abc import ABCMeta @@ -150,5 +149,7 @@ def wait(self, timeout_minutes=60): 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") + raise TimeoutError( + "Timeout: Process did not finish within the specified timeout period" + ) time.sleep(2) From b6ea7cca59678569a29046dbfda9c1a37fe8e66b Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 24 Oct 2024 19:04:01 +0200 Subject: [PATCH 05/12] Fix webapi test --- flow360/component/project.py | 6 +++--- tests/test_case_webapi.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index 1d0cd6a5f..4f7b2dac7 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -342,7 +342,7 @@ def _run( return destination_obj def run_surface_mesher( - self, params: SimulationParams, draft_name: str = "SurfaceMesh", run_async: bool = False + self, params: SimulationParams, draft_name: str = "SurfaceMesh", run_async: bool = True ): """Run surface mesher with the provided params or defaults""" self._check_initialized() @@ -357,7 +357,7 @@ def run_surface_mesher( ) def run_volume_mesher( - self, params: SimulationParams, draft_name: str = "VolumeMesh", run_async: bool = False + self, params: SimulationParams, draft_name: str = "VolumeMesh", run_async: bool = True ): """Run volume mesher with the provided params or defaults""" self._check_initialized() @@ -371,7 +371,7 @@ def run_volume_mesher( params=params, target=VolumeMeshV2, draft_name=draft_name, run_async=run_async ) - def run_case(self, params: SimulationParams, draft_name: str = "Case", run_async: bool = False): + def run_case(self, params: SimulationParams, draft_name: str = "Case", run_async: bool = True): """Run project with the provided params""" self._check_initialized() 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() From b9ca4a7134d7f6e32d719befb5a0dcd42fd60d3b Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 24 Oct 2024 19:28:45 +0200 Subject: [PATCH 06/12] Fix examples, fix from_cloud --- examples/project_from_cloud_geometry.py | 35 ++++++++++++++++++++----- examples/project_from_file_geometry.py | 2 +- flow360/component/project.py | 2 +- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/examples/project_from_cloud_geometry.py b/examples/project_from_cloud_geometry.py index d6369701c..7b6da3c10 100644 --- a/examples/project_from_cloud_geometry.py +++ b/examples/project_from_cloud_geometry.py @@ -4,26 +4,47 @@ 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.unit_system import SI_unit_system +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-5159c384-fc07-4bd1-b30f-e4ea0dcafa2d") +project = Project.from_cloud("prj-f3569ba5-16a3-4e41-bfd2-b8840df79835") print(project.get_simulation_json()) +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=1, - surface_edge_growth_rate=1.4, - surface_max_edge_length=1.0111, + 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.set_default_params(params) -project.run_case() +project.run_surface_mesher(params=params, draft_name="Case of Simple Airplane from Python") diff --git a/examples/project_from_file_geometry.py b/examples/project_from_file_geometry.py index 686b4494d..41df1dbb9 100644 --- a/examples/project_from_file_geometry.py +++ b/examples/project_from_file_geometry.py @@ -51,4 +51,4 @@ ], ) -case = project.run_case(params=params, draft_name="Case of Simple Airplane from Python") +case = project.run_surface_mesher(params=params, draft_name="Case of Simple Airplane from Python") diff --git a/flow360/component/project.py b/flow360/component/project.py index 4f7b2dac7..0f6c1fc15 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -218,7 +218,7 @@ def from_cloud(cls, project_id: str): elif root_type == RootType.VOLUME_MESH: root_asset = VolumeMeshV2.from_cloud(meta.root_item_id) - project = Project(info=meta, solver_version=root_asset.solver_version) + project = Project(metadata=meta, solver_version=root_asset.solver_version) if not root_asset: raise Flow360ValueError(f"Couldn't retrieve root asset for {project_id}") From 6372e0e146dc819cfdc3c53902b89a7aeda3f53b Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 24 Oct 2024 19:31:58 +0200 Subject: [PATCH 07/12] Add case fork option --- examples/project_from_cloud_volume_mesh.py | 3 +- examples/project_from_file_volume_mesh.py | 3 +- flow360/component/project.py | 33 ++++++++++++++++++---- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/examples/project_from_cloud_volume_mesh.py b/examples/project_from_cloud_volume_mesh.py index 8dd9d1726..ee72d9ae8 100644 --- a/examples/project_from_cloud_volume_mesh.py +++ b/examples/project_from_cloud_volume_mesh.py @@ -26,5 +26,4 @@ ], ) -project.set_default_params(params) -project.run_case() +project.run_case(params=params) diff --git a/examples/project_from_file_volume_mesh.py b/examples/project_from_file_volume_mesh.py index 52e468545..8c70fcbdd 100644 --- a/examples/project_from_file_volume_mesh.py +++ b/examples/project_from_file_volume_mesh.py @@ -38,5 +38,4 @@ ], ) -project.set_default_params(params) -project.run_case() +project.run_case(params=params) diff --git a/flow360/component/project.py b/flow360/component/project.py index 0f6c1fc15..ac03256c5 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -341,8 +341,13 @@ def _run( return destination_obj + # pylint: disable=too-many-arguments def run_surface_mesher( - self, params: SimulationParams, draft_name: str = "SurfaceMesh", run_async: bool = True + self, + params: SimulationParams, + draft_name: str = "SurfaceMesh", + run_async: bool = True, + fork: bool = False, ): """Run surface mesher with the provided params or defaults""" self._check_initialized() @@ -353,11 +358,16 @@ def run_surface_mesher( ) self._surface_mesh = self._run( - params=params, target=SurfaceMesh, draft_name=draft_name, run_async=run_async + params=params, target=SurfaceMesh, draft_name=draft_name, run_async=run_async, fork=fork ) + # pylint: disable=too-many-arguments def run_volume_mesher( - self, params: SimulationParams, draft_name: str = "VolumeMesh", run_async: bool = True + self, + params: SimulationParams, + draft_name: str = "VolumeMesh", + run_async: bool = True, + fork: bool = True, ): """Run volume mesher with the provided params or defaults""" self._check_initialized() @@ -368,13 +378,24 @@ def run_volume_mesher( ) self._volume_mesh = self._run( - params=params, target=VolumeMeshV2, draft_name=draft_name, run_async=run_async + params=params, + target=VolumeMeshV2, + draft_name=draft_name, + run_async=run_async, + fork=fork, ) - def run_case(self, params: SimulationParams, draft_name: str = "Case", run_async: bool = True): + # pylint: disable=too-many-arguments + def run_case( + self, + params: SimulationParams, + draft_name: str = "Case", + run_async: bool = True, + fork: bool = True, + ): """Run project with the provided params""" self._check_initialized() self._case = self._run( - params=params, target=Case, draft_name=draft_name, run_async=run_async + params=params, target=Case, draft_name=draft_name, run_async=run_async, fork=fork ) From 6f6648fdc6ec20e4dbd89773a96c0a8dbf785056 Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 24 Oct 2024 20:06:55 +0200 Subject: [PATCH 08/12] Switched docstrings to numpy style --- flow360/component/project.py | 320 ++++++++++++++++++++++++++--------- 1 file changed, 241 insertions(+), 79 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index ac03256c5..71ff900a2 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -32,24 +32,42 @@ from flow360.component.volume_mesh import VolumeMeshV2 from flow360.exceptions import Flow360FileError, Flow360ValueError, Flow360WebError -# This is used before all resources are moved to V2 API as AssetBase subclasses AssetOrResource = Union[type[AssetBase], type[Flow360Resource]] RootAsset = Union[Geometry, VolumeMeshV2] class RootType(Enum): """ - Type of a root object of the project + 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" - # SURFACE_MESH = "SurfaceMesh" - supported in a future iteration class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): """ - Metadata of the project + 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") @@ -61,15 +79,27 @@ class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): class Project(pd.BaseModel): """ - Project class containing the interface for creating and running simulations + 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. + geometry : Optional[Geometry] + Cached geometry asset, initialized on-demand. + volume_mesh : Optional[VolumeMeshV2] + Cached volume mesh asset, initialized on-demand. + surface_mesh : Optional[SurfaceMesh] + Cached surface mesh asset, initialized on-demand. + case : Optional[Case] + Cached case asset, initialized on-demand. """ metadata: ProjectMeta = pd.Field() solver_version: str = pd.Field(frozen=True) - # Right now the cached assets are all related to the - # root asset - we will have full project tree traversal - # in a future iteration. _geometry: Optional[Geometry] = pd.PrivateAttr(None) _volume_mesh: Optional[VolumeMeshV2] = pd.PrivateAttr(None) _surface_mesh: Optional[SurfaceMesh] = pd.PrivateAttr(None) @@ -78,63 +108,107 @@ class Project(pd.BaseModel): @property def geometry(self) -> Geometry: """ - Getter for the current project's geometry asset + Returns the geometry asset of the project. - Returns: Geometry asset if present, otherwise raises Flow360ValueError + Raises + ------ + Flow360ValueError + If the geometry asset is not available for the project. + + Returns + ------- + Geometry + The geometry asset. """ if not self._geometry: raise Flow360ValueError("Geometry asset is not available for this project") - return self._geometry @property def surface_mesh(self) -> SurfaceMesh: """ - Getter for the current project's surface mesh asset + Returns the surface mesh asset of the project. - Returns: Surface mesh asset if present, otherwise raises Flow360ValueError + Raises + ------ + Flow360ValueError + If the surface mesh asset is not available for the project. + + Returns + ------- + SurfaceMesh + The surface mesh asset. """ if not self._surface_mesh: raise Flow360ValueError("Surface mesh asset is not available for this project") - return self._surface_mesh @property def volume_mesh(self) -> VolumeMeshV2: """ - Getter for the current project's volume mesh asset + Returns the volume mesh asset of the project. - Returns: Volume mesh asset if present, otherwise raises Flow360ValueError + Raises + ------ + Flow360ValueError + If the volume mesh asset is not available for the project. + + Returns + ------- + VolumeMeshV2 + The volume mesh asset. """ if not self._volume_mesh: raise Flow360ValueError("Volume mesh asset is not available for this project") - return self._volume_mesh @property def case(self) -> Case: """ - Getter for the current project's case asset + Returns the case asset of the project. - Returns: Case asset if present, otherwise raises Flow360ValueError + Raises + ------ + Flow360ValueError + If the case asset is not available for the project. + + Returns + ------- + Case + The case asset. """ if not self._case: raise Flow360ValueError("Case asset is not available for this project") - return self._case @classmethod def _detect_root_type(cls, file): + """ + Detects the root 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." ) @@ -150,89 +224,105 @@ def from_file( tags: List[str] = None, ): """ - Populates project data from a file - - Args: - name (): Name of the new project - file (): - solver_version (): - length_unit (): - tags (): - - Returns: - + 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_root_type(file) - if root_type == RootType.GEOMETRY: - # Create a draft geometry asset and submit draft = Geometry.from_file(file, name, solver_version, length_unit, tags) root_asset = draft.submit() elif root_type == RootType.VOLUME_MESH: - # Create a draft volume mesh asset and submit 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) - if root_type == RootType.GEOMETRY: project._geometry = root_asset elif root_type == RootType.VOLUME_MESH: project._volume_mesh = root_asset - return project @classmethod def from_cloud(cls, project_id: str): - """Load project from cloud, only load the root asset for now""" + """ + 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) - project = Project(metadata=meta, solver_version=root_asset.solver_version) - if not root_asset: raise Flow360ValueError(f"Couldn't retrieve root asset for {project_id}") - if root_type == RootType.GEOMETRY: project._geometry = root_asset elif root_type == RootType.VOLUME_MESH: project._volume_mesh = root_asset - return project def _check_initialized(self): """ - Check if the Project instance has been initialized correctly + 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: raise Flow360ValueError( @@ -241,33 +331,37 @@ def _check_initialized(self): def get_simulation_json(self) -> dict: """ - Get default simulation JSON for the project based on the root asset type + Returns the default simulation JSON for the project based on the root asset type. + + Returns + ------- + dict + The simulation JSON for the project. + + 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") - root_api = None - if root_type == RootType.GEOMETRY: root_api = RestApi(GeometryInterface.endpoint, id=root_id) elif root_type == RootType.VOLUME_MESH: root_api = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_id) - resp = root_api.get(method="simulation/file", params={"type": "simulation"}) - if not isinstance(resp, dict) or "simulationJson" not in resp: raise Flow360WebError("Root item type or ID is missing from project metadata") - simulation_json = json.loads(resp["simulationJson"]) - return simulation_json - # pylint: disable=too-many-locals, too-many-arguments + # pylint: disable=too-many-arguments, too-many-locals def _run( self, params: SimulationParams, @@ -276,6 +370,33 @@ def _run( fork: bool = False, run_async: bool = True, ): + """ + 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.get_simulation_json() cache_key = "private_attribute_asset_cache" @@ -341,7 +462,6 @@ def _run( return destination_obj - # pylint: disable=too-many-arguments def run_surface_mesher( self, params: SimulationParams, @@ -349,19 +469,34 @@ def run_surface_mesher( run_async: bool = True, fork: bool = False, ): - """Run surface mesher with the provided params or defaults""" + """ + Runs the surface mesher for the project. + + Parameters + ---------- + params : SimulationParams + Simulation parameters for running the mesher. + draft_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). + + 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 ran by projects with a geometry root asset" + "Surface mesher can only be run by projects with a geometry root asset" ) - self._surface_mesh = self._run( params=params, target=SurfaceMesh, draft_name=draft_name, run_async=run_async, fork=fork ) - # pylint: disable=too-many-arguments def run_volume_mesher( self, params: SimulationParams, @@ -369,14 +504,30 @@ def run_volume_mesher( run_async: bool = True, fork: bool = True, ): - """Run volume mesher with the provided params or defaults""" + """ + Runs the volume mesher for the project. + + Parameters + ---------- + params : SimulationParams + Simulation parameters for running the mesher. + draft_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). + + 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 ran by projects with a geometry root asset" + "Volume mesher can only be run by projects with a geometry root asset" ) - self._volume_mesh = self._run( params=params, target=VolumeMeshV2, @@ -385,7 +536,6 @@ def run_volume_mesher( fork=fork, ) - # pylint: disable=too-many-arguments def run_case( self, params: SimulationParams, @@ -393,9 +543,21 @@ def run_case( run_async: bool = True, fork: bool = True, ): - """Run project with the provided params""" + """ + 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). + """ self._check_initialized() - self._case = self._run( params=params, target=Case, draft_name=draft_name, run_async=run_async, fork=fork ) From 9ba5587bee8a37191788f1094fe8503dd5cda41d Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Fri, 25 Oct 2024 17:04:00 +0200 Subject: [PATCH 09/12] Fix PR feedback --- examples/geometry_to_surface_mesh_V2.py | 38 ------------ examples/project_from_cloud_geometry.py | 4 +- examples/project_from_cloud_volume_mesh.py | 3 +- examples/project_from_file_geometry.py | 8 +-- examples/project_from_file_volume_mesh.py | 5 +- flow360/__init__.py | 3 +- flow360/component/project.py | 71 +++++++++++----------- flow360/component/utils.py | 8 +++ flow360/version.py | 1 + 9 files changed, 51 insertions(+), 90 deletions(-) delete mode 100644 examples/geometry_to_surface_mesh_V2.py 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 index 7b6da3c10..8234b0d35 100644 --- a/examples/project_from_cloud_geometry.py +++ b/examples/project_from_cloud_geometry.py @@ -18,7 +18,7 @@ dev.active() project = Project.from_cloud("prj-f3569ba5-16a3-4e41-bfd2-b8840df79835") -print(project.get_simulation_json()) +print(project.get_root_simulation_json()) geometry = project.geometry geometry.show_available_groupings(verbose_mode=True) @@ -47,4 +47,4 @@ ], ) -project.run_surface_mesher(params=params, draft_name="Case of Simple Airplane from Python") +project.generate_surface_mesh(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 index ee72d9ae8..c1485f3a5 100644 --- a/examples/project_from_cloud_volume_mesh.py +++ b/examples/project_from_cloud_volume_mesh.py @@ -12,7 +12,7 @@ dev.active() project = Project.from_cloud("prj-e8c6c7eb-c18b-4c15-bac8-edf5aaf9b155") -print(project.get_simulation_json()) +print(project.get_root_simulation_json()) volume_mesh = project.volume_mesh @@ -20,7 +20,6 @@ params = SimulationParams( operating_condition=AerospaceCondition(velocity_magnitude=100 * u.m / u.s), models=[ - Fluid(), Wall(entities=[volume_mesh["fluid/wall"]]), Freestream(entities=[volume_mesh["fluid/farfield"]]), ], diff --git a/examples/project_from_file_geometry.py b/examples/project_from_file_geometry.py index 41df1dbb9..6541d5482 100644 --- a/examples/project_from_file_geometry.py +++ b/examples/project_from_file_geometry.py @@ -18,11 +18,7 @@ fl.Env.dev.active() -SOLVER_VERSION = "workbench-24.9.3" - -project = Project.from_file( - Airplane.geometry, name="airplane-geometry-python-upload", solver_version=SOLVER_VERSION -) +project = Project.from_file(Airplane.geometry, name="airplane-geometry-python-upload") geometry = project.geometry geometry.show_available_groupings(verbose_mode=True) @@ -51,4 +47,4 @@ ], ) -case = project.run_surface_mesher(params=params, draft_name="Case of Simple Airplane from Python") +case = project.generate_surface_mesh(params=params, name="Case of Simple Airplane from Python") diff --git a/examples/project_from_file_volume_mesh.py b/examples/project_from_file_volume_mesh.py index 8c70fcbdd..734931bd8 100644 --- a/examples/project_from_file_volume_mesh.py +++ b/examples/project_from_file_volume_mesh.py @@ -19,10 +19,7 @@ OM6wing.get_files() # Creating and uploading a volume mesh from file project = Project.from_file( - OM6wing.mesh_filename, - name="wing-volume-mesh-python-upload", - solver_version="workbench-24.9.3", - tags=["python"], + OM6wing.mesh_filename, name="wing-volume-mesh-python-upload", tags=["python"] ) volume_mesh = project.volume_mesh 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/project.py b/flow360/component/project.py index 71ff900a2..0e743bb4a 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -9,7 +9,7 @@ import pydantic as pd -from flow360 import Case, SurfaceMesh +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 @@ -26,6 +26,7 @@ from flow360.component.simulation.web.draft import Draft from flow360.component.utils import ( SUPPORTED_GEOMETRY_FILE_PATTERNS, + SUPPORTED_MESH_FILE_PATTERNS, MeshNameParser, match_file_pattern, ) @@ -87,14 +88,6 @@ class Project(pd.BaseModel): Metadata of the project. solver_version : str Version of the solver being used. - geometry : Optional[Geometry] - Cached geometry asset, initialized on-demand. - volume_mesh : Optional[VolumeMeshV2] - Cached volume mesh asset, initialized on-demand. - surface_mesh : Optional[SurfaceMesh] - Cached surface mesh asset, initialized on-demand. - case : Optional[Case] - Cached case asset, initialized on-demand. """ metadata: ProjectMeta = pd.Field() @@ -104,6 +97,8 @@ class Project(pd.BaseModel): _volume_mesh: Optional[VolumeMeshV2] = pd.PrivateAttr(None) _surface_mesh: Optional[SurfaceMesh] = pd.PrivateAttr(None) _case: Optional[Case] = pd.PrivateAttr(None) + _root_webapi: Optional[RestApi] = pd.PrivateAttr(None) + _project_webapi: Optional[RestApi] = pd.PrivateAttr(None) @property def geometry(self) -> Geometry: @@ -209,17 +204,22 @@ def _detect_root_type(cls, file): return RootType.VOLUME_MESH except Flow360FileError: pass + raise Flow360FileError( - f"{file} is not a geometry or volume mesh file required for project initialization." + f"{file} is not a geometry or volume mesh file required for project initialization. " + "Accepted formats are: " + f"{SUPPORTED_GEOMETRY_FILE_PATTERNS} (geometry)" + f"{SUPPORTED_MESH_FILE_PATTERNS} (volume mesh)" ) # pylint: disable=too-many-arguments @classmethod + @pd.validate_call def from_file( cls, file: str = None, name: str = None, - solver_version: str = None, + solver_version: str = __solver_version__, length_unit: LengthUnitType = "m", tags: List[str] = None, ): @@ -265,13 +265,17 @@ def from_file( 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._geometry = root_asset + project._root_webapi = RestApi(GeometryInterface.endpoint, id=root_asset.id) elif root_type == RootType.VOLUME_MESH: project._volume_mesh = root_asset + project._root_webapi = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_asset.id) return project @classmethod + @pd.validate_call def from_cloud(cls, project_id: str): """ Loads a project from the cloud. @@ -307,12 +311,15 @@ def from_cloud(cls, project_id: str): elif root_type == RootType.VOLUME_MESH: root_asset = VolumeMeshV2.from_cloud(meta.root_item_id) project = Project(metadata=meta, solver_version=root_asset.solver_version) + project._project_webapi = project_api if not root_asset: raise Flow360ValueError(f"Couldn't retrieve root asset for {project_id}") if root_type == RootType.GEOMETRY: project._geometry = root_asset + project._root_webapi = RestApi(GeometryInterface.endpoint, id=root_asset.id) elif root_type == RootType.VOLUME_MESH: project._volume_mesh = root_asset + project._root_webapi = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_asset.id) return project def _check_initialized(self): @@ -329,7 +336,7 @@ def _check_initialized(self): "Project is not initialized - use Project.from_file or Project.from_cloud" ) - def get_simulation_json(self) -> dict: + def get_root_simulation_json(self) -> dict: """ Returns the default simulation JSON for the project based on the root asset type. @@ -350,14 +357,9 @@ def get_simulation_json(self) -> dict: 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") - root_api = None - if root_type == RootType.GEOMETRY: - root_api = RestApi(GeometryInterface.endpoint, id=root_id) - elif root_type == RootType.VOLUME_MESH: - root_api = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_id) - resp = root_api.get(method="simulation/file", params={"type": "simulation"}) + resp = self._root_webapi.get(method="simulation/file", params={"type": "simulation"}) if not isinstance(resp, dict) or "simulationJson" not in resp: - raise Flow360WebError("Root item type or ID is missing from project metadata") + raise Flow360WebError("Couldn't retrieve default simulation JSON for the project") simulation_json = json.loads(resp["simulationJson"]) return simulation_json @@ -397,7 +399,7 @@ def _run( If the simulation parameters lack required length unit information, or if the root asset (Geometry or VolumeMesh) is not initialized. """ - defaults = self.get_simulation_json() + defaults = self.get_root_simulation_json() cache_key = "private_attribute_asset_cache" length_key = "project_length_unit" @@ -434,21 +436,13 @@ def _run( ).submit() entity_info = root_asset.entity_info - registry = params.used_entity_registry - old_draft_entities = entity_info.draft_entities - for _, old_entity in enumerate(old_draft_entities): - try: - registry.find_by_naming_pattern(old_entity.name) - except ValueError: - continue - 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) - RestApi(ProjectInterface.endpoint, id=self.metadata.id).patch( + self._project_webapi.patch( json={ "lastOpenItemId": destination_id, "lastOpenItemType": target.__name__, @@ -462,10 +456,11 @@ def _run( return destination_obj - def run_surface_mesher( + @pd.validate_call + def generate_surface_mesh( self, params: SimulationParams, - draft_name: str = "SurfaceMesh", + name: str = "SurfaceMesh", run_async: bool = True, fork: bool = False, ): @@ -476,7 +471,7 @@ def run_surface_mesher( ---------- params : SimulationParams Simulation parameters for running the mesher. - draft_name : str, optional + name : str, optional Name of the draft (default is "SurfaceMesh"). run_async : bool, optional Whether to run the mesher asynchronously (default is True). @@ -494,13 +489,14 @@ def run_surface_mesher( "Surface mesher can only be run by projects with a geometry root asset" ) self._surface_mesh = self._run( - params=params, target=SurfaceMesh, draft_name=draft_name, run_async=run_async, fork=fork + params=params, target=SurfaceMesh, draft_name=name, run_async=run_async, fork=fork ) - def run_volume_mesher( + @pd.validate_call + def generate_volume_mesh( self, params: SimulationParams, - draft_name: str = "VolumeMesh", + name: str = "VolumeMesh", run_async: bool = True, fork: bool = True, ): @@ -511,7 +507,7 @@ def run_volume_mesher( ---------- params : SimulationParams Simulation parameters for running the mesher. - draft_name : str, optional + name : str, optional Name of the draft (default is "VolumeMesh"). run_async : bool, optional Whether to run the mesher asynchronously (default is True). @@ -531,11 +527,12 @@ def run_volume_mesher( self._volume_mesh = self._run( params=params, target=VolumeMeshV2, - draft_name=draft_name, + draft_name=name, run_async=run_async, fork=fork, ) + @pd.validate_call def run_case( self, params: SimulationParams, diff --git a/flow360/component/utils.py b/flow360/component/utils.py index 3f76797d0..3ce9881af 100644 --- a/flow360/component/utils.py +++ b/flow360/component/utils.py @@ -50,6 +50,14 @@ ".ipt", ] +SUPPORTED_MESH_FILE_PATTERNS = [ + ".cgns", + ".stl", + ".ugrid", + ".b8.ugrid", + ".lb8.ugrid", +] + def match_file_pattern(patterns, filename): """ 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" From 3b7aa7058e93995566189c245e366cd4e3222352 Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Mon, 28 Oct 2024 12:07:36 +0100 Subject: [PATCH 10/12] Fix PR feedback #2 --- examples/project_from_cloud_geometry.py | 3 +- examples/project_from_cloud_volume_mesh.py | 1 - examples/project_from_file_geometry.py | 4 +- ...roject_from_file_geometry_multiple_runs.py | 67 +++++ flow360/component/project.py | 279 ++++++++++++++---- flow360/component/utils.py | 105 +++++++ 6 files changed, 389 insertions(+), 70 deletions(-) create mode 100644 examples/project_from_file_geometry_multiple_runs.py diff --git a/examples/project_from_cloud_geometry.py b/examples/project_from_cloud_geometry.py index 8234b0d35..e6c3bc8e3 100644 --- a/examples/project_from_cloud_geometry.py +++ b/examples/project_from_cloud_geometry.py @@ -18,7 +18,6 @@ dev.active() project = Project.from_cloud("prj-f3569ba5-16a3-4e41-bfd2-b8840df79835") -print(project.get_root_simulation_json()) geometry = project.geometry geometry.show_available_groupings(verbose_mode=True) @@ -47,4 +46,4 @@ ], ) -project.generate_surface_mesh(params=params, name="Case of Simple Airplane from Python") +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 index c1485f3a5..40ec60cdb 100644 --- a/examples/project_from_cloud_volume_mesh.py +++ b/examples/project_from_cloud_volume_mesh.py @@ -12,7 +12,6 @@ dev.active() project = Project.from_cloud("prj-e8c6c7eb-c18b-4c15-bac8-edf5aaf9b155") -print(project.get_root_simulation_json()) volume_mesh = project.volume_mesh diff --git a/examples/project_from_file_geometry.py b/examples/project_from_file_geometry.py index 6541d5482..e3923550c 100644 --- a/examples/project_from_file_geometry.py +++ b/examples/project_from_file_geometry.py @@ -18,7 +18,7 @@ fl.Env.dev.active() -project = Project.from_file(Airplane.geometry, name="airplane-geometry-python-upload") +project = Project.from_file(Airplane.geometry, name="Python Project (Geometry, from file)") geometry = project.geometry geometry.show_available_groupings(verbose_mode=True) @@ -47,4 +47,4 @@ ], ) -case = project.generate_surface_mesh(params=params, name="Case of Simple Airplane from Python") +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..a5185bb3a --- /dev/null +++ b/examples/project_from_file_geometry_multiple_runs.py @@ -0,0 +1,67 @@ +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, 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()], + ), + 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.available_surface_meshes() +print(ids) diff --git a/flow360/component/project.py b/flow360/component/project.py index 0e743bb4a..c74fde6e6 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -5,7 +5,7 @@ # ProjectMeta instances as FieldInfo, I'd rather not have this line import json from enum import Enum -from typing import List, Optional, Union +from typing import Iterable, List, Optional, Union import pydantic as pd @@ -28,6 +28,7 @@ SUPPORTED_GEOMETRY_FILE_PATTERNS, SUPPORTED_MESH_FILE_PATTERNS, MeshNameParser, + ProjectAssetCache, match_file_pattern, ) from flow360.component.volume_mesh import VolumeMeshV2 @@ -78,6 +79,11 @@ class ProjectMeta(pd.BaseModel, extra=pd.Extra.allow): 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. @@ -93,17 +99,20 @@ class Project(pd.BaseModel): metadata: ProjectMeta = pd.Field() solver_version: str = pd.Field(frozen=True) - _geometry: Optional[Geometry] = pd.PrivateAttr(None) - _volume_mesh: Optional[VolumeMeshV2] = pd.PrivateAttr(None) - _surface_mesh: Optional[SurfaceMesh] = pd.PrivateAttr(None) - _case: Optional[Case] = pd.PrivateAttr(None) + _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. + Returns the geometry asset of the project. There is always only one geometry asset per project. Raises ------ @@ -115,12 +124,44 @@ def geometry(self) -> Geometry: Geometry The geometry asset. """ - if not self._geometry: - raise Flow360ValueError("Geometry asset is not available for this project") - return self._geometry + 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) -> SurfaceMesh: + def surface_mesh(self): """ Returns the surface mesh asset of the project. @@ -134,12 +175,42 @@ def surface_mesh(self) -> SurfaceMesh: SurfaceMesh The surface mesh asset. """ - if not self._surface_mesh: - raise Flow360ValueError("Surface mesh asset is not available for this project") - return self._surface_mesh + 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) -> VolumeMeshV2: + def volume_mesh(self): """ Returns the volume mesh asset of the project. @@ -153,12 +224,33 @@ def volume_mesh(self) -> VolumeMeshV2: VolumeMeshV2 The volume mesh asset. """ - if not self._volume_mesh: - raise Flow360ValueError("Volume mesh asset is not available for this project") - return self._volume_mesh + return self.get_volume_mesh() + + def get_case(self, asset_id: str = None) -> Case: + """ + Returns the 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) -> Case: + def case(self): """ Returns the case asset of the project. @@ -172,14 +264,56 @@ def case(self) -> Case: Case The case asset. """ - if not self._case: - raise Flow360ValueError("Case asset is not available for this project") - return self._case + return self.get_case() + + def available_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 available_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 available_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_root_type(cls, file): + def _detect_asset_type_from_file(cls, file): """ - Detects the root type of a file based on its name or pattern. + Detects the asset type of a file based on its name or pattern. Parameters ---------- @@ -250,7 +384,7 @@ def from_file( If the project cannot be initialized from the file. """ root_asset = None - root_type = cls._detect_root_type(file) + 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() @@ -267,11 +401,12 @@ def from_file( project = Project(metadata=ProjectMeta(**info), solver_version=root_asset.solver_version) project._project_webapi = project_api if root_type == RootType.GEOMETRY: - project._geometry = root_asset + project._root_asset = root_asset project._root_webapi = RestApi(GeometryInterface.endpoint, id=root_asset.id) elif root_type == RootType.VOLUME_MESH: - project._volume_mesh = root_asset + project._root_asset = root_asset project._root_webapi = RestApi(VolumeMeshInterfaceV2.endpoint, id=root_asset.id) + project._get_root_simulation_json() return project @classmethod @@ -310,16 +445,17 @@ def from_cloud(cls, project_id: str): root_asset = Geometry.from_cloud(meta.root_item_id) elif root_type == RootType.VOLUME_MESH: root_asset = VolumeMeshV2.from_cloud(meta.root_item_id) - project = Project(metadata=meta, solver_version=root_asset.solver_version) - project._project_webapi = project_api 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._geometry = root_asset + project._root_asset = root_asset project._root_webapi = RestApi(GeometryInterface.endpoint, id=root_asset.id) elif root_type == RootType.VOLUME_MESH: - project._volume_mesh = root_asset + 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): @@ -331,19 +467,14 @@ def _check_initialized(self): Flow360ValueError If the project is not initialized. """ - if not self.metadata or not self.solver_version: + if not self.metadata or not self.solver_version or not self._root_asset: raise Flow360ValueError( - "Project is not initialized - use Project.from_file or Project.from_cloud" + "Project not initialized - use Project.from_file or Project.from_cloud" ) - def get_root_simulation_json(self) -> dict: + def _get_root_simulation_json(self): """ - Returns the default simulation JSON for the project based on the root asset type. - - Returns - ------- - dict - The simulation JSON for the project. + Loads the default simulation JSON for the project based on the root asset type. Raises ------ @@ -361,7 +492,7 @@ def get_root_simulation_json(self) -> dict: 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"]) - return simulation_json + self._root_simulation_json = simulation_json # pylint: disable=too-many-arguments, too-many-locals def _run( @@ -371,6 +502,7 @@ def _run( draft_name: str = None, fork: bool = False, run_async: bool = True, + solver_version: str = None, ): """ Runs a simulation for the project. @@ -399,7 +531,8 @@ def _run( If the simulation parameters lack required length unit information, or if the root asset (Geometry or VolumeMesh) is not initialized. """ - defaults = self.get_root_simulation_json() + + defaults = self._root_simulation_json cache_key = "private_attribute_asset_cache" length_key = "project_length_unit" @@ -415,29 +548,19 @@ def _run( length_unit ) - root_asset = None - root_type = self.metadata.root_item_type - - if root_type == RootType.GEOMETRY: - root_asset = self._geometry - elif root_type == RootType.VOLUME_MESH: - root_asset = self._volume_mesh - - if not root_asset: - raise Flow360ValueError("Root asset is not initialized") + 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=root_type.value, - solver_version=self.solver_version, + 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_info = root_asset.entity_info with model_attribute_unlock(params.private_attribute_asset_cache, "project_entity_info"): - params.private_attribute_asset_cache.project_entity_info = entity_info + params.private_attribute_asset_cache.project_entity_info = root_asset.entity_info draft.update_simulation_params(params) destination_id = draft.run_up_to_target_asset(target) @@ -463,6 +586,7 @@ def generate_surface_mesh( name: str = "SurfaceMesh", run_async: bool = True, fork: bool = False, + solver_version: str = None, ): """ Runs the surface mesher for the project. @@ -477,6 +601,8 @@ def generate_surface_mesh( 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 ------ @@ -488,8 +614,15 @@ def generate_surface_mesh( raise Flow360ValueError( "Surface mesher can only be run by projects with a geometry root asset" ) - self._surface_mesh = self._run( - params=params, target=SurfaceMesh, draft_name=name, run_async=run_async, fork=fork + 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 @@ -499,6 +632,7 @@ def generate_volume_mesh( name: str = "VolumeMesh", run_async: bool = True, fork: bool = True, + solver_version: str = None, ): """ Runs the volume mesher for the project. @@ -513,6 +647,8 @@ def generate_volume_mesh( 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 ------ @@ -524,12 +660,15 @@ def generate_volume_mesh( raise Flow360ValueError( "Volume mesher can only be run by projects with a geometry root asset" ) - self._volume_mesh = self._run( - params=params, - target=VolumeMeshV2, - draft_name=name, - run_async=run_async, - fork=fork, + 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 @@ -539,6 +678,7 @@ def run_case( draft_name: str = "Case", run_async: bool = True, fork: bool = True, + solver_version: str = None, ): """ Runs a case for the project. @@ -553,8 +693,17 @@ def run_case( 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 = self._run( - params=params, target=Case, draft_name=draft_name, run_async=run_async, fork=fork + 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/utils.py b/flow360/component/utils.py index 3ce9881af..81efc00c5 100644 --- a/flow360/component/utils.py +++ b/flow360/component/utils.py @@ -7,6 +7,7 @@ from enum import Enum from functools import wraps from tempfile import NamedTemporaryFile +from typing import Generic, Iterable, Protocol, TypeVar import zstandard as zstd @@ -561,3 +562,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_id : str, optional + The ID of the currently set asset. + cache : dict of str to AssetT + Dictionary storing assets with their IDs as keys. + """ + + current_id: str = None + 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.cache: + raise Flow360ValueError("Cache is empty, no assets are available") + + asset = self.cache.get(self.current_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.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.cache[asset.id] = asset + self.current_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.cache: + raise Flow360ValueError(f"{asset_id} is not available in the project.") + + self.current_id = asset_id From 99f2143dc7504ed7a226d440d9ef37e627561955 Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Mon, 28 Oct 2024 18:09:17 +0100 Subject: [PATCH 11/12] Fix PR feedback #3 --- flow360/component/project.py | 14 ++++++++++++- flow360/component/simulation/entity_info.py | 4 ++-- flow360/component/utils.py | 22 ++++++++++----------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index c74fde6e6..905cea328 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -19,6 +19,8 @@ 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 @@ -559,8 +561,18 @@ def _run( 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 = root_asset.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) 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/utils.py b/flow360/component/utils.py index 81efc00c5..6e5cf61b1 100644 --- a/flow360/component/utils.py +++ b/flow360/component/utils.py @@ -587,14 +587,14 @@ class ProjectAssetCache(Generic[AssetT]): Attributes ---------- - current_id : str, optional + current_asset_id : str, optional The ID of the currently set asset. - cache : dict of str to AssetT + asset_cache : dict of str to AssetT Dictionary storing assets with their IDs as keys. """ - current_id: str = None - cache: dict[str, AssetT] = {} + current_asset_id: str = None + asset_cache: dict[str, AssetT] = {} def get_asset(self, asset_id: str = None) -> AssetT: """ @@ -615,10 +615,10 @@ def get_asset(self, asset_id: str = None) -> AssetT: Flow360ValueError If the cache is empty or if the asset is not found. """ - if not self.cache: + if not self.asset_cache: raise Flow360ValueError("Cache is empty, no assets are available") - asset = self.cache.get(self.current_id if not asset_id else asset_id) + 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.") @@ -634,7 +634,7 @@ def get_ids(self) -> Iterable[str]: Iterable[str] An iterable of asset IDs. """ - return list(self.cache.keys()) + return list(self.asset_cache.keys()) def add_asset(self, asset: AssetT): """ @@ -645,8 +645,8 @@ def add_asset(self, asset: AssetT): asset : AssetT The asset to add. Must have a unique `id` attribute. """ - self.cache[asset.id] = asset - self.current_id = asset.id + self.asset_cache[asset.id] = asset + self.current_asset_id = asset.id def set_id(self, asset_id: str): """ @@ -662,7 +662,7 @@ def set_id(self, asset_id: str): Flow360ValueError If the specified `asset_id` does not exist in the cache. """ - if asset_id not in self.cache: + if asset_id not in self.asset_cache: raise Flow360ValueError(f"{asset_id} is not available in the project.") - self.current_id = asset_id + self.current_asset_id = asset_id From d07cfe8777c1386e17c47bd53f7199ae10ce6d0e Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Tue, 29 Oct 2024 10:56:23 +0100 Subject: [PATCH 12/12] Fix PR feedback #4 --- examples/project_from_cloud_volume_mesh.py | 20 ++++++++--- ...roject_from_file_geometry_multiple_runs.py | 22 ++++++++++-- examples/project_from_file_volume_mesh.py | 8 +++-- flow360/component/project.py | 15 ++++---- flow360/component/utils.py | 34 ++++++++++++++----- 5 files changed, 72 insertions(+), 27 deletions(-) diff --git a/examples/project_from_cloud_volume_mesh.py b/examples/project_from_cloud_volume_mesh.py index 40ec60cdb..84cf27bbc 100644 --- a/examples/project_from_cloud_volume_mesh.py +++ b/examples/project_from_cloud_volume_mesh.py @@ -1,7 +1,12 @@ +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, Wall -from flow360.component.simulation.models.volume_models import Fluid +from flow360.component.simulation.models.surface_models import ( + Freestream, + SymmetryPlane, + Wall, +) from flow360.component.simulation.operating_condition.operating_condition import ( AerospaceCondition, ) @@ -11,7 +16,7 @@ dev.active() -project = Project.from_cloud("prj-e8c6c7eb-c18b-4c15-bac8-edf5aaf9b155") +project = Project.from_cloud("prj-b8eb4cc7-4fb8-4baa-9bcd-f1cf6d73163d") volume_mesh = project.volume_mesh @@ -19,9 +24,14 @@ params = SimulationParams( operating_condition=AerospaceCondition(velocity_magnitude=100 * u.m / u.s), models=[ - Wall(entities=[volume_mesh["fluid/wall"]]), - Freestream(entities=[volume_mesh["fluid/farfield"]]), + 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_multiple_runs.py b/examples/project_from_file_geometry_multiple_runs.py index a5185bb3a..254136c8b 100644 --- a/examples/project_from_file_geometry_multiple_runs.py +++ b/examples/project_from_file_geometry_multiple_runs.py @@ -4,13 +4,16 @@ MeshingDefaults, MeshingParams, ) -from flow360.component.simulation.meshing_param.volume_params import AutomatedFarfield +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 ReferenceGeometry +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 @@ -33,6 +36,19 @@ 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), @@ -63,5 +79,5 @@ assert surface_mesh_1.id != surface_mesh_2.id # Check available surface mesh IDs in the project -ids = project.available_surface_meshes() +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 index 734931bd8..86c4b5089 100644 --- a/examples/project_from_file_volume_mesh.py +++ b/examples/project_from_file_volume_mesh.py @@ -1,3 +1,5 @@ +from matplotlib.pyplot import show + import flow360 as fl import flow360.component.simulation.units as u from flow360.component.project import Project @@ -6,7 +8,6 @@ SymmetryPlane, Wall, ) -from flow360.component.simulation.models.volume_models import Fluid from flow360.component.simulation.operating_condition.operating_condition import ( AerospaceCondition, ) @@ -28,7 +29,6 @@ params = SimulationParams( operating_condition=AerospaceCondition(velocity_magnitude=100 * u.m / u.s), models=[ - Fluid(), Wall(entities=[volume_mesh["1"]]), Freestream(entities=[volume_mesh["3"]]), SymmetryPlane(entities=[volume_mesh["2"]]), @@ -36,3 +36,7 @@ ) 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/component/project.py b/flow360/component/project.py index 905cea328..a12dd0c5d 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -28,7 +28,6 @@ from flow360.component.simulation.web.draft import Draft from flow360.component.utils import ( SUPPORTED_GEOMETRY_FILE_PATTERNS, - SUPPORTED_MESH_FILE_PATTERNS, MeshNameParser, ProjectAssetCache, match_file_pattern, @@ -165,7 +164,7 @@ def get_surface_mesh(self, asset_id: str = None) -> SurfaceMesh: @property def surface_mesh(self): """ - Returns the surface mesh asset of the project. + Returns the last used surface mesh asset of the project. Raises ------ @@ -214,7 +213,7 @@ def get_volume_mesh(self, asset_id: str = None) -> VolumeMeshV2: @property def volume_mesh(self): """ - Returns the volume mesh asset of the project. + Returns the last used volume mesh asset of the project. Raises ------ @@ -230,7 +229,7 @@ def volume_mesh(self): def get_case(self, asset_id: str = None) -> Case: """ - Returns the case asset of the project. + Returns the last used case asset of the project. Parameters ---------- @@ -268,7 +267,7 @@ def case(self): """ return self.get_case() - def available_surface_meshes(self) -> Iterable[str]: + def get_cached_surface_meshes(self) -> Iterable[str]: """ Returns the available IDs of surface meshes in the project @@ -284,7 +283,7 @@ def available_surface_meshes(self) -> Iterable[str]: return self._surface_mesh_cache.get_ids() - def available_volume_meshes(self): + def get_cached_volume_meshes(self): """ Returns the available IDs of volume meshes in the project @@ -301,7 +300,7 @@ def available_volume_meshes(self): return self._volume_mesh_cache.get_ids() - def available_cases(self): + def get_cached_cases(self): """ Returns the available IDs of cases in the project @@ -345,7 +344,7 @@ def _detect_asset_type_from_file(cls, file): f"{file} is not a geometry or volume mesh file required for project initialization. " "Accepted formats are: " f"{SUPPORTED_GEOMETRY_FILE_PATTERNS} (geometry)" - f"{SUPPORTED_MESH_FILE_PATTERNS} (volume mesh)" + f"{MeshNameParser.all_patterns(mesh_type='volume')} (volume mesh)" ) # pylint: disable=too-many-arguments diff --git a/flow360/component/utils.py b/flow360/component/utils.py index 6e5cf61b1..c56ab4675 100644 --- a/flow360/component/utils.py +++ b/flow360/component/utils.py @@ -2,12 +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, Protocol, TypeVar +from typing import Generic, Iterable, Literal, Protocol, TypeVar import zstandard as zstd @@ -51,14 +52,6 @@ ".ipt", ] -SUPPORTED_MESH_FILE_PATTERNS = [ - ".cgns", - ".stl", - ".ugrid", - ".b8.ugrid", - ".lb8.ugrid", -] - def match_file_pattern(patterns, filename): """ @@ -540,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): """