Source code for flow360.component.case

"""
Case component
"""

# pylint: disable=too-many-lines
from __future__ import annotations

import json
import os
import tempfile
from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union

import pydantic as pd
import pydantic.v1 as pd_v1
from flow360_schema.models.simulation.simulation_params import SimulationParams

from .. import error_messages
from ..cloud.flow360_requests import (
    MoveCaseItem,
    MoveToFolderRequest,
    RenameAssetRequestV2,
)
from ..cloud.rest_api import RestApi
from ..cloud.utils import is_valid_uuid
from ..environment import current_environment
from ..exceptions import Flow360RuntimeError, Flow360ValidationError, Flow360ValueError
from ..log import log
from .folder import Folder
from .interfaces import (
    CaseInterface,
    CaseInterfaceV2,
    FolderInterface,
    VolumeMeshInterface,
)
from .resource_base import (
    AssetMetaBaseModel,
    AssetMetaBaseModelV2,
    Flow360Resource,
    Flow360ResourceListBase,
    Flow360Status,
    ResourceDraft,
    before_submit_only,
    is_object_cloud_resource,
)
from .results.base_results import ResultTarGZModel
from .results.case_results import (
    ActuatorDiskResultCSVModel,
    AeroacousticsResultCSVModel,
    BETForcesRadialDistributionResultCSVModel,
    BETForcesResultCSVModel,
    CaseDownloadable,
    CFLResultCSVModel,
    CustomForceResultModel,
    ForceDistributionsResultModel,
    LegacyForceDistributionResultCSVModel,
    LinearResidualsResultCSVModel,
    MaxResidualLocationResultCSVModel,
    MinMaxStateResultCSVModel,
    MonitorsResultModel,
    NonlinearResidualsResultCSVModel,
    PorousMediumResultCSVModel,
    RendersResultModel,
    ResultBaseModel,
    ResultsDownloaderSettings,
    SurfaceForcesResultCSVModel,
    SurfaceHeatTransferResultCSVModel,
    TotalForcesResultCSVModel,
    UserDefinedDynamicsResultModel,
    XSlicingForceDistributionResultCSVModel,
    YSlicingForceDistributionResultCSVModel,
)
from .simulation import services
from .simulation.web.asset_webapi import CaseWebApi
from .utils import (
    _local_download_file_list_overwrite,
    _local_download_overwrite,
    shared_account_confirm_proceed,
    validate_type,
)
from .v1.flow360_params import Flow360Params, UnvalidatedFlow360Params
from .validator import Validator

if TYPE_CHECKING:
    from flow360.component.volume_mesh import VolumeMeshV2


class CaseBase:
    """
    Case Base component
    """

    def copy(
        self,
        name: str = None,
        params: Flow360Params = None,
        solver_version: str = None,
        tags: List[str] = None,
    ) -> CaseDraft:
        """
        Alias for retry case
        :param name:
        :param params:
        :param tags:
        :return:
        """

        return self.retry(name, params, solver_version=solver_version, tags=tags)

    # pylint: disable=no-member
    def retry(
        self,
        name: str = None,
        params: Flow360Params = None,
        solver_version: str = None,
        tags: List[str] = None,
    ) -> CaseDraft:
        """
        Retry case
        :param name:
        :param params:
        :param tags:
        :return:
        """

        name = name or self.name or self.info.name
        params = params or self.params.copy(deep=True)
        new_case = Case.create(
            name, params, other_case=self, solver_version=solver_version, tags=tags
        )
        return new_case

    def continuation(
        self, name: str = None, params: Flow360Params = None, tags: List[str] = None
    ) -> CaseDraft:
        """
        Alias for fork a case to continue simulation
        :param name:
        :param params:
        :param tags:
        :return:
        """

        return self.fork(name, params, tags)

    # pylint: disable=no-member
    def fork(
        self, name: str = None, params: Flow360Params = None, tags: List[str] = None
    ) -> CaseDraft:
        """
        Fork a case to continue simulation
        :param name:
        :param params:
        :param tags:
        :return:
        """

        name = name or self.name or self.info.name
        params = params or self.params.copy(deep=True)
        return Case.create(name, params, parent_case=self, tags=tags)


class CaseMeta(AssetMetaBaseModel):
    """
    CaseMeta data component
    """

    id: str = pd_v1.Field(alias="caseId")
    case_mesh_id: str = pd_v1.Field(alias="caseMeshId")
    parent_id: Union[str, None] = pd_v1.Field(alias="parentId")
    status: Flow360Status = pd_v1.Field()

    # Resource status change, revisit when updating the case class
    @pd_v1.validator("status")
    @classmethod
    def set_status_type(cls, value: Flow360Status):
        """set_status_type when case uploaded"""
        if value is Flow360Status.UPLOADED:
            return Flow360Status.CASE_UPLOADED
        return value

    def to_case(self) -> Case:
        """
        returns Case object from case meta info
        """
        return Case(self.id)


class CaseMetaV2(AssetMetaBaseModelV2):
    """
    CaseMetaV2 component
    """

    id: str = pd.Field(alias="caseId")
    status: Flow360Status = pd.Field()

    def to_case(self) -> Case:
        """
        returns Case object from case meta info
        """
        return Case(self.id)


class ComputeUsage(pd.BaseModel):
    """
    Compute recorded for a case.

    ``type`` is ``"FLEX_CREDIT"`` or ``"VGPU_ALLOCATION"``. ``value`` is in ``unit``:
    ``"FLEX_CREDIT"`` for FlexCredit cases, and the contracted GPU equivalent
    (``"H200_EQUIVALENT"`` or ``"A100_EQUIVALENT"``) for Virtual GPU cases, which is not
    the physical GPU model. Fractions are kept, and a recorded zero is a real zero.
    """

    model_config = pd.ConfigDict(frozen=True)

    type: str
    value: float
    unit: str


class ResourceUsage(pd.BaseModel):
    """
    Usage recorded for a resource.

    ``compute`` is None when the cloud has no compute usage on record, for example for a case
    that has not finished, or for a Virtual GPU case submitted before usage reporting existed.
    """

    model_config = pd.ConfigDict(frozen=True)

    compute: Optional[ComputeUsage]


# pylint: disable=too-many-instance-attributes
class CaseDraft(CaseBase, ResourceDraft):
    """
    Case Draft component (before submission)
    """

    # pylint: disable=too-many-arguments
    def __init__(
        self,
        name: str,
        params: Flow360Params,
        volume_mesh_id: str = None,
        tags: List[str] = None,
        parent_id: str = None,
        other_case: Case = None,
        parent_case: Case = None,
        solver_version: str = None,
    ):
        self.name = name
        self.params = params
        self.volume_mesh_id = volume_mesh_id
        self.parent_case = parent_case
        self.parent_id = parent_id
        self.other_case = other_case
        self.tags = tags
        self.solver_version = solver_version
        self._id = None
        self._submitted_case = None
        ResourceDraft.__init__(self)

        self.validate_case_inputs()

    def __str__(self):
        return self.params.__str__()

    @property
    def params(self) -> Flow360Params:
        """
        returns case params
        """
        return self._params

    @params.setter
    def params(self, value: Flow360Params):
        """
        sets case params (before submit only)
        """
        if not isinstance(value, Flow360Params) and not isinstance(value, UnvalidatedFlow360Params):
            raise Flow360ValueError("params are not of type Flow360Params.")
        self._params = value

    @property
    def name(self) -> str:
        """
        returns case name
        """
        return self._name

    @name.setter
    def name(self, value) -> str:
        """
        sets case name
        """
        self._name = value

    @property
    def volume_mesh_id(self):
        """
        returns volume mesh id
        """
        return self._volume_mesh_id

    @volume_mesh_id.setter
    def volume_mesh_id(self, value):
        """
        sets volume mesh id
        """
        self._volume_mesh_id = value

    def to_case(self) -> Case:
        """Return Case from CaseDraft (must be after .submit())

        Returns
        -------
        Case
            Case representation

        Raises
        ------
        RuntimeError
            Raises error when case is before submission, i.e., is in draft state
        """
        if not self.is_cloud_resource():
            raise Flow360RuntimeError(
                f"Case name={self.name} is in draft state. Run .submit() before calling this function."
            )
        return Case(self.id)

    @before_submit_only
    def submit(self, force_submit: bool = False) -> Case:
        """
        Submits case to cloud for running.
        """
        assert self.name
        assert self.volume_mesh_id or self.other_case or self.parent_id or self.parent_case
        assert self.params

        self.validate_case_inputs(pre_submit_checks=True)

        if not shared_account_confirm_proceed():
            raise Flow360ValueError("User aborted resource submit.")

        volume_mesh_id = self.volume_mesh_id
        parent_id = self.parent_id
        if parent_id is not None:
            self.parent_case = Case(self.parent_id)

        if isinstance(self.parent_case, CaseDraft):
            self.parent_case = self.parent_case.to_case()

        if isinstance(self.other_case, CaseDraft):
            self.other_case = self.other_case.to_case()

        if self.other_case is not None and self.other_case.has_parent():
            self.parent_case = self.other_case.parent

        if self.parent_case is not None:
            parent_id = self.parent_case.id
            volume_mesh_id = self.parent_case.volume_mesh_id

            if (
                self.solver_version is not None
                and self.parent_case.solver_version != self.solver_version
            ):
                raise Flow360RuntimeError(
                    error_messages.change_solver_version_error(
                        self.parent_case.solver_version, self.solver_version
                    )
                )
            self.solver_version = self.parent_case.solver_version

        volume_mesh_id = volume_mesh_id or self.other_case.volume_mesh_id

        if self.solver_version is None:
            volume_mesh_info = AssetMetaBaseModel(
                **RestApi(
                    VolumeMeshInterface.endpoint,
                    id=volume_mesh_id,
                    environment_provider=current_environment,
                ).get()
            )
            self.solver_version = volume_mesh_info.solver_version

        is_valid_uuid(volume_mesh_id)
        self.validator_api(
            self.params,
            volume_mesh_id=volume_mesh_id,
            solver_version=self.solver_version,
            raise_on_error=(not force_submit),
        )

        data = {
            "name": self.name,
            "meshId": volume_mesh_id,
            "runtimeParams": self.params.json(),
            "tags": self.tags,
            "parentId": parent_id,
        }

        if self.solver_version is not None:
            data["solverVersion"] = self.solver_version

        resp = RestApi(CaseInterface.endpoint, environment_provider=current_environment).post(
            json=data,
            path=f"volumemeshes/{volume_mesh_id}/case",
        )
        info = CaseMeta(**resp)
        # setting _id will disable "WARNING: You have not submitted..." warning message
        self._id = info.id

        self._submitted_case = Case(self.id)

        kwargs = {}
        if self._submitted_case.project_id is not None:
            kwargs = {"project_id": self._submitted_case.project_id}

        log.info(f"Case successfully submitted: {self._submitted_case.short_description(**kwargs)}")
        return self._submitted_case

    def validate_case_inputs(self, pre_submit_checks=False):
        """
        validates case inputs (before submit only)
        """
        if self.volume_mesh_id is not None and self.other_case is not None:
            raise Flow360ValueError("You cannot specify both volume_mesh_id AND other_case.")

        if self.parent_id is not None and self.parent_case is not None:
            raise Flow360ValueError("You cannot specify both parent_id AND parent_case.")

        if self.parent_id is not None or self.parent_case is not None:
            if self.volume_mesh_id is not None or self.other_case is not None:
                raise Flow360ValueError(
                    "You cannot specify volume_mesh_id OR other_case when parent case provided."
                )

        is_valid_uuid(self.volume_mesh_id, allow_none=True)

        if pre_submit_checks:
            is_object_cloud_resource(self.other_case)
            is_object_cloud_resource(self.parent_case)

    @classmethod
    def validator_api(
        cls,
        params: Flow360Params,
        volume_mesh_id,
        solver_version: str = None,
        raise_on_error: bool = True,
    ):
        """
        validation api: validates case parameters before submitting
        """
        return Validator.CASE.validate(
            params,
            mesh_id=volume_mesh_id,
            solver_version=solver_version,
            raise_on_error=raise_on_error,
        )


# pylint: disable=too-many-instance-attributes,too-many-public-methods
[docs] class Case(CaseBase, Flow360Resource): """ Case component """ _manifest_path = "visualize/manifest/manifest.json" _cloud_resource_type_name = "Case" _web_api_v2_class = Flow360Resource # pylint: disable=redefined-builtin def __init__(self, id: str): super().__init__( interface=CaseInterface, meta_class=CaseMeta, id=id, ) self._params = None self._raw_params = None self._results = CaseResultsModel(case=self) self._manifest = None # _web_api_v2 handles all V2 communications for Case self._web_api_v2 = self._web_api_v2_class( interface=CaseInterfaceV2, meta_class=CaseMetaV2, id=id, ) @classmethod def _from_meta(cls, meta: CaseMeta): validate_type(meta, "meta", CaseMeta) case = cls(id=meta.id) case._set_meta(meta) return case
[docs] def get_simulation_params(self): """ returns simulation params """ # pylint: disable=import-outside-toplevel from botocore.exceptions import ClientError as CloudFileNotFoundError try: params_as_dict = self._parse_json_from_cloud("simulation.json") except CloudFileNotFoundError as err: raise Flow360ValueError( "Simulation params not found for this case. It is likely it was created with old interface." f" Original error: {str(err)}" ) from err # if the params come from GUI, it can contain data that is not conformal with SimulationParams thus cleaning param, errors, _ = services.validate_model( params_as_dict=params_as_dict, validated_by=services.ValidationCalledBy.LOCAL, root_item_type=None, validation_level=None, ) if errors is not None: raise Flow360ValidationError( f"Error found in simulation params. The param may be created by an incompatible version. {errors}", ) return param
@property def params(self) -> Union[Flow360Params, SimulationParams]: """ returns case params """ if self._params is None: try: self._params = self.get_simulation_params() return self._params except Flow360ValueError: pass self._raw_params = json.loads(self.get(method="runtimeParams")["content"]) try: with tempfile.NamedTemporaryFile( mode="w", suffix=".json", delete=False ) as temp_file: json.dump(self._raw_params, temp_file) self._params = Flow360Params(temp_file.name) except pd_v1.ValidationError as err: raise Flow360ValidationError(error_messages.params_fetching_error(err)) from err return self._params @property def params_as_dict(self) -> dict: """ returns case params as dictionary """ if self._raw_params is None: self._raw_params = json.loads(self.get(method="runtimeParams")["content"]) return self._raw_params
[docs] def has_parent(self) -> bool: """Check if case has parent case Returns ------- bool True when case has parent, False otherwise """ return self.info.parent_id is not None
@property def parent(self) -> Case: """parent case Returns ------- Case parent case object Raises ------ RuntimeError When case does not have parent """ if self.has_parent(): return Case(self.info.parent_id) raise Flow360RuntimeError("Case does not have parent case.") @property def info(self) -> CaseMetaV2: """ returns metadata info for case """ return super().info @property def info_v2(self) -> CaseMetaV2: """ returns metadata v2 info for case """ return self._web_api_v2.info @property def project_id(self) -> Optional[str]: """Returns the project id of the case if case was run with V2 interface.""" if isinstance(self.info, CaseMeta): try: return self.info.projectId except AttributeError: # No project id at all in V1 resources return None if isinstance(self.info, CaseMetaV2): return self.info.project_id raise ValueError("Case info is not of type CaseMeta or CaseMetaV2") @property def tags(self) -> List[str]: """ get case tags """ return self._web_api_v2.info.tags @property def volume_mesh(self) -> VolumeMeshV2: """ returns volume mesh """ from_cache = self.local_resource_cache[self.volume_mesh_id] if from_cache is not None: return from_cache # pylint: disable=import-outside-toplevel,cyclic-import from .volume_mesh import VolumeMeshV2 return VolumeMeshV2(self.volume_mesh_id) @property def volume_mesh_id(self): """ returns volume mesh id """ return self.info.case_mesh_id @property def results(self) -> CaseResultsModel: """ returns results object to managing case results """ return self._results @property def usage(self) -> ResourceUsage: """ Usage recorded for the case, fetched from the cloud on every access. ``usage.compute`` stays None until the case finishes, so call :meth:`wait` first. Read it once (``usage = case.usage``) rather than once per field. """ return ResourceUsage.model_validate(CaseWebApi(self.id).get_usage()["usage"]) # pylint: disable=no-member
[docs] def is_steady(self): """ returns True when case is steady state """ if isinstance(self.params, Flow360Params): return self.params.time_stepping.time_step_size == "inf" return self.params.is_steady()
[docs] def has_actuator_disks(self): """ returns True when case has actuator disk """ if isinstance(self.params, Flow360Params): return self.params.actuator_disks is not None and len(self.params.actuator_disks) > 0 return self.params.has_actuator_disks()
[docs] def has_bet_disks(self): """ returns True when case has BET disk """ if isinstance(self.params, Flow360Params): return self.params.bet_disks is not None and len(self.params.bet_disks) > 0 return self.params.has_bet_disks()
[docs] def has_isosurfaces(self): """ returns True when case has isosurfaces """ if isinstance(self.params, Flow360Params): return self.params.iso_surface_output is not None return self.params.has_isosurfaces()
[docs] def has_monitors(self): """ returns True when case has monitors """ if isinstance(self.params, Flow360Params): return self.params.monitor_output is not None return self.params.has_monitors()
[docs] def has_volume_output(self): """ returns True when case has volume output """ if isinstance(self.params, Flow360Params): return self.params.volume_output is not None return self.params.has_volume_output()
[docs] def has_aeroacoustics(self): """ returns True when case has aeroacoustics """ if isinstance(self.params, Flow360Params): return self.params.aeroacoustic_output is not None return self.params.has_aeroacoustics()
[docs] def has_user_defined_dynamics(self): """ returns True when case has user defined dynamics """ if isinstance(self.params, Flow360Params): return self.params.user_defined_dynamics is not None return self.params.has_user_defined_dynamics()
[docs] def has_custom_forces(self): """ returns True when case has custom forces """ if isinstance(self.params, Flow360Params): return False return self.params.has_custom_forces()
[docs] def has_force_distributions(self): """ returns True when case has force distributions """ if isinstance(self.params, Flow360Params): return False return self.params.has_force_distributions()
[docs] def has_renders(self): """ returns True when case has render outputs """ if isinstance(self.params, Flow360Params): return False return any( getattr(output, "output_type", None) == "RenderOutput" for output in (getattr(self.params, "outputs", None) or []) )
[docs] def move_to_folder(self, folder: Folder): """ Move the current case to the specified folder. Parameters ---------- folder : Folder The destination folder where the item will be moved. Returns ------- self Returns the modified item after it has been moved to the new folder. Notes ----- This method sends a REST API request to move the current item to the specified folder. The `folder` parameter should be an instance of the `Folder` class with a valid ID. """ RestApi(FolderInterface.endpoint, environment_provider=current_environment).put( MoveToFolderRequest(dest_folder_id=folder.id, items=[MoveCaseItem(id=self.id)]).dict(), method="move", ) return self
[docs] def rename(self, new_name: str): """ Rename the current case. Parameters ---------- new_name : str The new name for the case. """ RestApi(CaseInterfaceV2.endpoint, environment_provider=current_environment).patch( RenameAssetRequestV2(name=new_name).dict(), method=self.id )
[docs] def delete(self): # pylint: disable=arguments-differ """ Delete the current case. Goes through the v2 endpoint. The v1 one runs the S3 cleanup inline before it stops the pipeline job, so a failed cleanup leaves a deleted case still running. """ self._web_api_v2.delete()
[docs] def stop(self): """ Stop the running case, the same way Stop does in the web UI. The solver stops at an upcoming step, its results are uploaded as usual, and the case finishes as ``completed``. This returns once the request is sent; call :meth:`wait` to block until the case finishes. As in the web UI, stopping a case re-creates its draft in the project. Raises ------ Flow360RuntimeError If the case is not running: it has not started yet, or it has already finished. """ CaseWebApi(self.id).stop()
@classmethod def _interface(cls): return CaseInterface @classmethod def _meta_class(cls): """ returns case meta info class: CaseMeta """ return CaseMeta @classmethod def _params_ancestor_id_name(cls): """ returns volumeMeshId name """ return "meshId"
[docs] @classmethod def from_cloud(cls, case_id: str): """ get case from cloud """ return cls(case_id)
[docs] @classmethod def from_local_storage(cls, local_storage_path, meta_data: CaseMeta) -> Case: """ Create a `Case` instance from local storage. Parameters ---------- local_storage_path : str The path to the local storage directory. meta_data : CaseMeta Case metadata containing: - id (str): The unique identifier for the case. - name (str): The name of the case. - user_id (str): The user ID associated with the case, can be "local". Returns ------- Case An instance of `Case` with data loaded from local storage. """ _local_download_file = _local_download_overwrite(local_storage_path, cls.__name__) _local_download_file_list = _local_download_file_list_overwrite(local_storage_path) case = cls._from_meta(meta_data) case._download_file = _local_download_file case.get_download_file_list = _local_download_file_list case._results = CaseResultsModel(case=case) case.results.set_local_storage(local_storage_path, keep_remote_structure=True) return case
# pylint: disable=too-many-arguments
[docs] @classmethod def create( cls, name: str, params: Flow360Params, volume_mesh_id: str = None, tags: List[str] = None, parent_id: str = None, other_case: Case = None, parent_case: Case = None, solver_version: str = None, ) -> CaseDraft: """ Create new case :param name: :param params: :param volume_mesh_id: :param other_case: :param tags: :param parent_id: :param parent_case: :return: """ assert name assert volume_mesh_id or other_case or parent_id or parent_case assert params if not isinstance(params, Flow360Params) and not isinstance( params, UnvalidatedFlow360Params ): raise Flow360ValueError("params are not of type Flow360Params.") new_case = CaseDraft( name=name, volume_mesh_id=volume_mesh_id, params=params.copy(), parent_id=parent_id, other_case=other_case, parent_case=parent_case, tags=tags, solver_version=solver_version, ) return new_case
# pylint: disable=unnecessary-lambda
[docs] class CaseResultsModel(pd.BaseModel): """ Pydantic models for case results """ case: Any = pd.Field() # tar.gz results: surfaces: ResultTarGZModel = pd.Field( default_factory=lambda: ResultTarGZModel(remote_file_name=CaseDownloadable.SURFACES.value) ) volumes: ResultTarGZModel = pd.Field( default_factory=lambda: ResultTarGZModel(remote_file_name=CaseDownloadable.VOLUMES.value) ) slices: ResultTarGZModel = pd.Field( default_factory=lambda: ResultTarGZModel(remote_file_name=CaseDownloadable.SLICES.value) ) isosurfaces: ResultTarGZModel = pd.Field( default_factory=lambda: ResultTarGZModel( remote_file_name=CaseDownloadable.ISOSURFACES.value ) ) # Rendered outputs (visualize/renders/): MP4 per render in mode="video", # or per-frame PNGs in mode="frames". Downloaded by render-output name. renders: RendersResultModel = pd.Field(default_factory=lambda: RendersResultModel()) monitors: MonitorsResultModel = pd.Field(MonitorsResultModel()) # convergence: nonlinear_residuals: NonlinearResidualsResultCSVModel = pd.Field( default_factory=lambda: NonlinearResidualsResultCSVModel() ) linear_residuals: LinearResidualsResultCSVModel = pd.Field( default_factory=lambda: LinearResidualsResultCSVModel() ) cfl: CFLResultCSVModel = pd.Field(default_factory=lambda: CFLResultCSVModel()) minmax_state: MinMaxStateResultCSVModel = pd.Field( default_factory=lambda: MinMaxStateResultCSVModel() ) max_residual_location: MaxResidualLocationResultCSVModel = pd.Field( default_factory=lambda: MaxResidualLocationResultCSVModel() ) # forces total_forces: TotalForcesResultCSVModel = pd.Field( default_factory=lambda: TotalForcesResultCSVModel() ) surface_forces: SurfaceForcesResultCSVModel = pd.Field( default_factory=lambda: SurfaceForcesResultCSVModel() ) actuator_disks: ActuatorDiskResultCSVModel = pd.Field( default_factory=lambda: ActuatorDiskResultCSVModel() ) bet_forces: BETForcesResultCSVModel = pd.Field( default_factory=lambda: BETForcesResultCSVModel() ) porous_media: PorousMediumResultCSVModel = pd.Field( default_factory=lambda: PorousMediumResultCSVModel() ) bet_forces_radial_distribution: BETForcesRadialDistributionResultCSVModel = pd.Field( default_factory=lambda: BETForcesRadialDistributionResultCSVModel() ) legacy_force_distribution: LegacyForceDistributionResultCSVModel = pd.Field( default_factory=lambda: LegacyForceDistributionResultCSVModel() ) x_slicing_force_distribution: XSlicingForceDistributionResultCSVModel = pd.Field( default_factory=lambda: XSlicingForceDistributionResultCSVModel() ) y_slicing_force_distribution: YSlicingForceDistributionResultCSVModel = pd.Field( default_factory=lambda: YSlicingForceDistributionResultCSVModel() ) # user defined: user_defined_dynamics: UserDefinedDynamicsResultModel = pd.Field( default_factory=lambda: UserDefinedDynamicsResultModel() ) custom_forces: CustomForceResultModel = pd.Field( default_factory=lambda: CustomForceResultModel() ) force_distributions: ForceDistributionsResultModel = pd.Field( default_factory=lambda: ForceDistributionsResultModel() ) # others surface_heat_transfer: SurfaceHeatTransferResultCSVModel = pd.Field( default_factory=lambda: SurfaceHeatTransferResultCSVModel() ) aeroacoustics: AeroacousticsResultCSVModel = pd.Field( default_factory=lambda: AeroacousticsResultCSVModel() ) local_storage: Optional[str] = None _downloader_settings: ResultsDownloaderSettings = pd.PrivateAttr(ResultsDownloaderSettings())
[docs] @pd.model_validator(mode="after") def pass_download_function(self): """ Pass download methods into fields of the case results. """ if self.case is None: raise ValueError("case (type Case) is required") if not isinstance(self.case, Case): raise TypeError("case must be of type Case") for field_name in self.__class__.model_fields: # pylint:disable = not-an-iterable value = getattr(self, field_name) if isinstance(value, ResultBaseModel): # pylint: disable=protected-access,no-member value._download_method = self.case._download_file # pylint: disable=protected-access,no-member value._get_params_method = lambda: self.case.params value.local_storage = self.local_storage if hasattr(value, "get_download_file_list_method"): value.get_download_file_list_method = self.case.get_download_file_list return self
# pylint: disable=protected-access,no-member
[docs] @pd.model_validator(mode="after") def pass_has_functions(self): """ Pass check to see if result is downloadable based on params """ has_function_map = { "actuator_disks": self.case.has_actuator_disks, "bet_forces": self.case.has_bet_disks, "bet_forces_radial_distribution": self.case.has_bet_disks, "isosurfaces": self.case.has_isosurfaces, "monitors": self.case.has_monitors, "volumes": self.case.has_volume_output, "aeroacoustics": self.case.has_aeroacoustics, "user_defined_dynamics": self.case.has_user_defined_dynamics, "custom_forces": self.case.has_custom_forces, "force_distributions": self.case.has_force_distributions, "renders": self.case.has_renders, } for field_name in self.__class__.model_fields: # pylint:disable = not-an-iterable value = getattr(self, field_name) if isinstance(value, ResultBaseModel): function = has_function_map.get(field_name, lambda: True) value._is_downloadable = function # pylint: disable=protected-access return self
def _execute_downloading(self): """ Download all specified and available results for the case """ for _, value in self.__dict__.items(): if isinstance(value, ResultBaseModel): # we download if explicitly set set_downloader(<result_name>=True), # or all=True but only when is not result=False # pylint: disable=no-member try_download = value.do_download is True if self._downloader_settings.all is True and value.do_download is not False: try_download = value._is_downloadable() is True if try_download is True: value.download( to_folder=self._downloader_settings.destination, overwrite=self._downloader_settings.overwrite, )
[docs] def set_destination( self, folder_name: str = None, use_case_name: bool = None, use_case_id: bool = None ): """ Set the destination for downloading files. Parameters ---------- folder_name : str, optional The name of the folder where files will be downloaded. use_case_name : bool, optional Whether to use the use case name for the destination. use_case_id : bool, optional Whether to use the use case ID for the destination. Raises ------ ValueError If more than one argument is provided or if no arguments are provided. """ # Check if only one argument is provided if sum(arg is not None for arg in [folder_name, use_case_name, use_case_id]) != 1: raise ValueError("Exactly one argument should be provided.") # pylint: disable=assigning-non-slot if folder_name is not None: self._downloader_settings.destination = folder_name # pylint: disable=no-member if use_case_name is True: self._downloader_settings.destination = self.case.name if use_case_id is True: self._downloader_settings.destination = self.case.id
# pylint: disable=too-many-arguments, too-many-locals, redefined-builtin
[docs] def download( self, surface: bool = None, volume: bool = None, slices: bool = None, isosurfaces: bool = None, monitors: bool = None, nonlinear_residuals: bool = None, linear_residuals: bool = None, cfl: bool = None, minmax_state: bool = None, max_residual_location: bool = None, surface_forces: bool = None, total_forces: bool = None, bet_forces: bool = None, bet_forces_radial_distribution: bool = None, actuator_disks: bool = None, legacy_force_distribution: bool = None, x_slicing_force_distribution: bool = None, y_slicing_force_distribution: bool = None, user_defined_dynamics: bool = None, custom_forces: bool = None, force_distributions: bool = None, aeroacoustics: bool = None, surface_heat_transfer: bool = None, all: bool = None, overwrite: bool = False, destination: str = None, ): """ Download result files associated with the case. Parameters ---------- surface : bool, optional Download surface result file if True. volume : bool, optional Download volume result file if True. nonlinear_residuals : bool, optional Download nonlinear residuals file if True. linear_residuals : bool, optional Download linear residuals file if True. cfl : bool, optional Download CFL file if True. minmax_state : bool, optional Download minmax state file if True. surface_forces : bool, optional Download surface forces file if True. total_forces : bool, optional Download total forces file if True. bet_forces : bool, optional Download BET (Blade Element Theory) forces file if True. bet_forces_radial_distribution : bool, optional Download BET (Blade Element Theory) forces radial distribution file if True. actuator_disk_output : bool, optional Download actuator disk output file if True. all : bool, optional Download all result files if True. Ignore file if explicitly set: <result_name>=False overwrite : bool, optional If True, overwrite existing files with the same name in the destination. destination : str, optional Location to save downloaded files. If None, files will be saved in the current directory under ID folder. """ # pylint: disable=assigning-non-slot self.surfaces.do_download = surface self.volumes.do_download = volume self.slices.do_download = slices self.isosurfaces.do_download = isosurfaces self.monitors.do_download = monitors self.nonlinear_residuals.do_download = nonlinear_residuals self.linear_residuals.do_download = linear_residuals self.cfl.do_download = cfl self.minmax_state.do_download = minmax_state self.max_residual_location.do_download = max_residual_location self.surface_forces.do_download = surface_forces self.total_forces.do_download = total_forces self.bet_forces.do_download = bet_forces self.bet_forces_radial_distribution.do_download = bet_forces_radial_distribution self.actuator_disks.do_download = actuator_disks self.legacy_force_distribution.do_download = legacy_force_distribution self.x_slicing_force_distribution.do_download = x_slicing_force_distribution self.y_slicing_force_distribution.do_download = y_slicing_force_distribution self.user_defined_dynamics.do_download = user_defined_dynamics self.custom_forces.do_download = custom_forces self.force_distributions.do_download = force_distributions self.aeroacoustics.do_download = aeroacoustics self.surface_heat_transfer.do_download = surface_heat_transfer self._downloader_settings.all = all self._downloader_settings.overwrite = overwrite if destination is not None: self.set_destination(folder_name=destination) self._execute_downloading()
[docs] def download_file_by_name(self, file_name, to_file=None, to_folder=".", overwrite: bool = True): """ Download file by name """ # pylint: disable=no-member return self.case._download_file( file_name=file_name, to_file=to_file, to_folder=to_folder, overwrite=overwrite )
[docs] def set_local_storage(self, local_storage: str, keep_remote_structure: bool = False): """ Set local storage for fetching data from. Used with Case.from_local_storage(...) Parameters ---------- local_storage : str Path to local folder keep_remote_structure : bool, optional When true, remote folder structure is assumed to be preserved, otherwise flat structure, by default False """ for field_name in self.__class__.model_fields: # pylint:disable = not-an-iterable value = getattr(self, field_name) if isinstance(value, ResultBaseModel): if keep_remote_structure is True: # pylint: disable=protected-access,no-member value.local_storage = os.path.dirname( os.path.join(local_storage, value._remote_path()) ) else: value.local_storage = local_storage
class CaseList(Flow360ResourceListBase): """ Case List component """ def __init__( self, mesh_id: str = None, from_cloud: bool = True, include_deleted: bool = False, limit=100 ): super().__init__( ancestor_id=mesh_id, from_cloud=from_cloud, include_deleted=include_deleted, limit=limit, resourceClass=Case, ) def filter(self): """ flitering list, not implemented yet """ raise NotImplementedError("Filters are not implemented yet") # resp = list(filter(lambda i: i['caseStatus'] != 'deleted', resp)) # pylint: disable=useless-parent-delegation def __getitem__(self, index) -> Case: """ returns CaseMeta info item of the list """ return super().__getitem__(index) def __iter__(self) -> Iterator[Case]: return super().__iter__()