Source code for flow360.component.volume_mesh

"""
Volume mesh component
"""

# pylint: disable=too-many-lines

from __future__ import annotations

import os.path
import threading
from enum import Enum
from functools import cached_property
from typing import Any, Iterator, List, Optional, Union

import numpy as np
import pydantic as pd_v2

# This will be split into two files in a future commit...
# For now we keep it to not mix new features with file
# structure refactors
import pydantic.v1 as pd

from flow360.cloud.compress_upload import compress_and_upload_chunks
from flow360.cloud.flow360_requests import (
    CopyExampleVolumeMeshRequest,
    LengthUnitType,
    NewVolumeMeshRequestV2,
)
from flow360.cloud.heartbeat import post_upload_heartbeat
from flow360.cloud.rest_api import RestApi
from flow360.component.simulation.folder import Folder
from flow360.component.utils import VolumeMeshFile
from flow360.component.v1.cloud.flow360_requests import NewVolumeMeshRequest
from flow360.component.v1.meshing.params import VolumeMeshingParams
from flow360.exceptions import (
    Flow360CloudFileError,
    Flow360FileError,
    Flow360NotImplementedError,
    Flow360RuntimeError,
    Flow360ValueError,
)
from flow360.flags import Flags
from flow360.log import log
from flow360.solver_version import Flow360Version

from .case import Case, CaseDraft
from .interfaces import VolumeMeshInterface, VolumeMeshInterfaceV2
from .resource_base import (
    AssetMetaBaseModel,
    AssetMetaBaseModelV2,
    Flow360Resource,
    Flow360ResourceListBase,
    ResourceDraft,
)
from .results.base_results import PerEntityResultCSVModel
from .simulation.primitives import GenericVolume, Surface
from .simulation.web.asset_base import AssetBase
from .types import COMMENTS
from .utils import (
    CompressionFormat,
    MeshFileFormat,
    MeshNameParser,
    UGRIDEndianness,
    shared_account_confirm_proceed,
    validate_type,
    zstd_compress,
)
from .v1.boundaries import NoSlipWall
from .v1.flow360_params import Flow360MeshParams, Flow360Params, _GenericBoundaryWrapper
from .v1.params_base import params_generic_validator
from .validator import Validator


def get_datatype(dataset):
    """
    Get datatype of dataset
    :param dataset:
    :return:
    """
    data_raw = np.empty(dataset.shape, dataset.dtype)
    dataset.read_direct(data_raw)
    data_str = "".join([chr(i) for i in dataset])
    return data_str


def get_no_slip_walls(params: Union[Flow360Params, Flow360MeshParams]):
    """
    Get wall boundary names
    :param params:
    :return:
    """
    assert params

    if (
        isinstance(params, Flow360MeshParams)
        and params.boundaries
        and params.boundaries.no_slip_walls
    ):
        return params.boundaries.no_slip_walls

    if isinstance(params, Flow360Params) and params.boundaries:
        return [
            wall_name
            for wall_name, wall in params.boundaries.dict().items()
            # pylint: disable=no-member
            if wall_name != COMMENTS and _GenericBoundaryWrapper(v=wall).v.type == NoSlipWall().type
        ]

    return []


def get_boundaries_from_sliding_interfaces(params: Union[Flow360Params, Flow360MeshParams]):
    """
    Get wall boundary names
    :param params:
    :return:
    """
    assert params
    res = []

    # Sliding interfaces are deprecated - we need to handle this somehow
    # if params.sliding_interfaces and params.sliding_interfaces.rotating_patches:
    #    res += params.sliding_interfaces.rotating_patches[:]
    # if params.sliding_interfaces and params.sliding_interfaces.stationary_patches:
    #    res += params.sliding_interfaces.stationary_patches[:]
    return res


# pylint: disable=too-many-branches
def get_boundaries_from_file(cgns_file: str, solver_version: str = None):
    """
    Get boundary names from CGNS file
    :param cgns_file:
    :param solver_version:
    :return:
    """
    import h5py  # pylint: disable=import-outside-toplevel

    names = []
    with h5py.File(cgns_file, "r") as h5_file:
        base = h5_file["Base"]
        for zone_name, zone in base.items():
            if zone_name == " data":
                continue
            if zone.attrs["label"].decode() != "Zone_t":
                continue
            zone_type = get_datatype(base[f"{zone_name}/ZoneType/ data"])
            if zone_type not in ["Structured", "Unstructured"]:
                continue
            for section_name, section in zone.items():
                if section_name == " data":
                    continue
                if "label" not in section.attrs:
                    continue
                if solver_version and Flow360Version(solver_version) < Flow360Version(
                    "release-22.2.1.0"
                ):
                    if section.attrs["label"].decode() != "Elements_t":
                        continue
                    element_type_tag = int(zone[f"{section_name}/ data"][0])
                    if element_type_tag in [5, 7]:
                        names.append(f"{zone_name}/{section_name}")
                    if element_type_tag == 20:
                        first_element_type_tag = zone[f"{section_name}/ElementConnectivity/ data"][
                            0
                        ]
                        if first_element_type_tag in [5, 7]:
                            names.append(f"{zone_name}/{section_name}")
                else:
                    if section.attrs["label"].decode() != "ZoneBC_t":
                        continue
                    for bc_name, bc_zone in section.items():
                        if bc_zone.attrs["label"].decode() == "BC_t":
                            names.append(f"{zone_name}/{bc_name}")

        return names


def validate_cgns(
    cgns_file: str, params: Union[Flow360Params, Flow360MeshParams], solver_version=None
):
    """
    Validate CGNS file
    :param cgns_file:
    :param params:
    :param solver_version:
    :return:
    """
    assert cgns_file
    assert params
    boundaries_in_file = get_boundaries_from_file(cgns_file, solver_version)
    boundaries_in_params = get_no_slip_walls(params) + get_boundaries_from_sliding_interfaces(
        params
    )
    boundaries_in_file = set(boundaries_in_file)
    boundaries_in_params = set(boundaries_in_params)

    if not boundaries_in_file.issuperset(boundaries_in_params):
        raise Flow360ValueError(
            "The following input boundary names from mesh json are not found in mesh:"
            + f" {' '.join(boundaries_in_params - boundaries_in_file)}."
            + f" Boundary names in cgns: {' '.join(boundaries_in_file)}"
            + f" Boundary names in params: {' '.join(boundaries_in_file)}"
        )
    log.info(
        f'Notice: {" ".join(boundaries_in_file - boundaries_in_params)} is '
        + "tagged as wall in cgns file, but not in input params"
    )


class VolumeMeshLog(Enum):
    """
    Volume mesh log
    """

    USER_LOG = "user.log"
    PY_LOG = "validateFlow360Mesh.py.log"


class VolumeMeshDownloadable(Enum):
    """
    Volume mesh downloadable files
    """

    CONFIG_JSON = "config.json"
    BOUNDING_BOX = "meshBoundaryBoundingBox.json"


# pylint: disable=E0213
class VolumeMeshMeta(AssetMetaBaseModel, extra=pd.Extra.allow):
    """
    VolumeMeshMeta component
    """

    id: str = pd.Field(alias="meshId")
    name: str = pd.Field(alias="meshName")
    created_at: str = pd.Field(alias="meshAddTime")
    surface_mesh_id: Optional[str] = pd.Field(alias="surfaceMeshId")
    mesh_params: Union[Flow360MeshParams, None, dict] = pd.Field(alias="meshParams")
    mesh_format: Union[MeshFileFormat, None] = pd.Field(alias="meshFormat")
    file_name: Union[str, None] = pd.Field(alias="fileName")
    endianness: UGRIDEndianness = pd.Field(alias="meshEndianness")
    compression: CompressionFormat = pd.Field(alias="meshCompression")
    boundaries: Union[List, None]

    @pd.validator("mesh_params", pre=True)
    def init_mesh_params(cls, value):
        """
        validator for mesh_params
        """
        return params_generic_validator(value, Flow360MeshParams)

    @pd.validator("endianness", pre=True)
    def init_endianness(cls, value):
        """
        validator for endianess
        """
        return UGRIDEndianness(value) or UGRIDEndianness.NONE

    @pd.validator("compression", pre=True)
    def init_compression(cls, value):
        """
        validator for compression
        """
        try:
            return CompressionFormat(value)
        except ValueError:
            return CompressionFormat.NONE

    def to_volume_mesh(self) -> VolumeMesh:
        """
        returns VolumeMesh object from volume mesh meta info
        """
        return VolumeMesh(self.id)


class VolumeMeshDraft(ResourceDraft):
    """
    Volume mesh draft component (before submit)
    """

    # pylint: disable=too-many-arguments, too-many-instance-attributes
    def __init__(
        self,
        file_name: str = None,
        params: Union[Flow360MeshParams, VolumeMeshingParams] = None,
        name: str = None,
        surface_mesh_id=None,
        tags: List[str] = None,
        solver_version=None,
        endianess: UGRIDEndianness = None,
        isascii: bool = False,
    ):
        if file_name is not None and not os.path.exists(file_name):
            raise Flow360FileError(f"File '{file_name}' not found.")

        if endianess is not None:
            raise Flow360NotImplementedError(
                "endianess selections not supported, it is inferred from filename"
            )

        if isascii is True:
            raise Flow360NotImplementedError("isascii not supported")

        self.params = None
        if params is not None:
            if not isinstance(params, Flow360MeshParams) and not isinstance(
                params, VolumeMeshingParams
            ):
                raise ValueError(
                    f"params={params} are not of type Flow360MeshParams OR VolumeMeshingParams"
                )
            self.params = params.copy(deep=True)

        if file_name is not None:
            mesh_parser = MeshNameParser(file_name)
            if not mesh_parser.is_valid_volume_mesh():
                raise Flow360ValueError(
                    f"Unsupported volume mesh file extensions: {mesh_parser.format.ext()}. "
                    f"Supported: [{MeshFileFormat.UGRID.ext()},{MeshFileFormat.CGNS.ext()}]."
                )

        if name is None and file_name is not None:
            name = os.path.splitext(os.path.basename(file_name))[0]

        self.file_name = file_name
        self.name = name
        self.surface_mesh_id = surface_mesh_id
        self.tags = tags
        self.solver_version = solver_version
        self._id = None
        self.compress_method = CompressionFormat.ZST
        ResourceDraft.__init__(self)

    def _submit_from_surface(self, force_submit: bool = False):
        self.validator_api(
            self.params, solver_version=self.solver_version, raise_on_error=(not force_submit)
        )
        body = {
            "name": self.name,
            "tags": self.tags,
            "surfaceMeshId": self.surface_mesh_id,
            "config": self.params.flow360_json(),
            "format": "cgns",
        }
        if Flags.beta_features() and self.params.version is not None:
            body["version"] = self.params.version

        if Flags.beta_features() and self.params.version is not None:
            if self.params.version == "v2":
                body["format"] = "aflr3"

        if self.solver_version:
            body["solverVersion"] = self.solver_version

        resp = RestApi(VolumeMeshInterface.endpoint).post(body)
        if not resp:
            return None

        info = VolumeMeshMeta(**resp)
        # setting _id will disable "WARNING: You have not submitted..." warning message
        self._id = info.id
        mesh = VolumeMesh(self.id)
        log.info(f"VolumeMesh successfully submitted: {mesh.short_description()}")
        return mesh

    # pylint: disable=protected-access, too-many-locals
    def _submit_upload_mesh(self, progress_callback=None):
        assert os.path.exists(self.file_name)

        mesh_parser = MeshNameParser(self.file_name)
        mesh_format = mesh_parser.format
        endianness = mesh_parser.endianness
        original_compression = mesh_parser.compression

        compression = (
            original_compression
            if original_compression != CompressionFormat.NONE
            else self.compress_method
        )

        if mesh_format is MeshFileFormat.CGNS:
            remote_file_name = "volumeMesh"
        else:
            remote_file_name = "mesh"
        remote_file_name = (
            f"{remote_file_name}{endianness.ext()}{mesh_format.ext()}{compression.ext()}"
        )

        name = self.name
        if name is None:
            name = os.path.splitext(os.path.basename(self.file_name))[0]

        if Flags.beta_features() and self.params is not None:
            req = NewVolumeMeshRequest(
                name=name,
                file_name=remote_file_name,
                tags=self.tags,
                format=mesh_format.value,
                endianness=endianness.value,
                compression=compression.value,
                params=self.params,
                solver_version=self.solver_version,
                version=self.params.version,
            )
        else:
            req = NewVolumeMeshRequest(
                name=name,
                file_name=remote_file_name,
                tags=self.tags,
                format=mesh_format.value,
                endianness=endianness.value,
                compression=compression.value,
                params=self.params,
                solver_version=self.solver_version,
            )
        resp = RestApi(VolumeMeshInterface.endpoint).post(req.dict())
        if not resp:
            return None

        info = VolumeMeshMeta(**resp)
        # setting _id will disable "WARNING: You have not submitted..." warning message
        self._id = info.id
        mesh = VolumeMesh(self.id)

        # parallel compress and upload
        if (
            original_compression == CompressionFormat.NONE
            and self.compress_method == CompressionFormat.BZ2
        ):
            upload_id = mesh.create_multipart_upload(remote_file_name)
            compress_and_upload_chunks(self.file_name, upload_id, mesh, remote_file_name)

        elif (
            original_compression == CompressionFormat.NONE
            and self.compress_method == CompressionFormat.ZST
        ):
            compressed_file_name = zstd_compress(self.file_name)
            mesh._upload_file(
                remote_file_name, compressed_file_name, progress_callback=progress_callback
            )
            os.remove(compressed_file_name)
        else:
            mesh._upload_file(remote_file_name, self.file_name, progress_callback=progress_callback)
        mesh._complete_upload(remote_file_name)

        log.info(f"VolumeMesh successfully uploaded: {mesh.short_description()}")
        return mesh

    def submit(self, progress_callback=None, force_submit: bool = False) -> VolumeMesh:
        """submit mesh to cloud

        Parameters
        ----------
        progress_callback : callback, optional
            Use for custom progress bar, by default None

        Returns
        -------
        VolumeMesh
            VolumeMesh object with id
        """

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

        if self.file_name is not None:
            return self._submit_upload_mesh(progress_callback)

        if self.surface_mesh_id is not None and self.name is not None and self.params is not None:
            return self._submit_from_surface(force_submit=force_submit)

        raise Flow360ValueError(
            "You must provide volume mesh file for upload or surface mesh Id with meshing parameters."
        )

    @classmethod
    def validator_api(
        cls, params: VolumeMeshingParams, solver_version=None, raise_on_error: bool = True
    ):
        """
        validation api: validates surface meshing parameters before submitting
        """
        return Validator.VOLUME_MESH.validate(
            params, solver_version=solver_version, raise_on_error=raise_on_error
        )


[docs] class VolumeMesh(Flow360Resource): """ Volume mesh component """ # pylint: disable=redefined-builtin def __init__(self, id: str): super().__init__( interface=VolumeMeshInterface, meta_class=VolumeMeshMeta, id=id, ) self.__mesh_params = None @classmethod def _from_meta(cls, meta: VolumeMeshMeta): validate_type(meta, "meta", VolumeMeshMeta) volume_mesh = cls(id=meta.id) volume_mesh._set_meta(meta) return volume_mesh @property def info(self) -> VolumeMeshMeta: return super().info @property def _mesh_params(self) -> Flow360MeshParams: """ returns mesh params """ if self.__mesh_params is None: self.__mesh_params = self.info.mesh_params return self.__mesh_params @property def no_slip_walls(self): """ returns mesh no_slip_walls """ if self._mesh_params is None: return None return self._mesh_params.boundaries.no_slip_walls @property def all_boundaries(self): """ returns mesh no_slip_walls """ return self.info.boundaries # pylint: disable=too-many-arguments,R0801 def download_file( self, file_name: Union[str, VolumeMeshDownloadable], to_file=None, to_folder=".", overwrite: bool = True, progress_callback=None, **kwargs, ): """ Download file from surface mesh :param file_name: :param to_file: :return: """ if isinstance(file_name, VolumeMeshDownloadable): file_name = file_name.value return super()._download_file( file_name, to_file=to_file, to_folder=to_folder, overwrite=overwrite, progress_callback=progress_callback, **kwargs, ) # pylint: disable=R0801 def download(self, to_file=None, to_folder=".", overwrite: bool = True): """ Download volume mesh file :param to_file: :return: """ status = self.status if not status.is_final(): log.warning(f"Cannot download file because status={status}") return None remote_file_name = self.info.file_name if remote_file_name is None: remote_file_name = self._remote_file_name() return super()._download_file( remote_file_name, to_file=to_file, to_folder=to_folder, overwrite=overwrite, ) # pylint: disable=arguments-differ def _complete_upload(self, remote_file_name): """ Complete volume mesh upload :return: """ resp = self.post({}, method=f"completeUpload?fileName={remote_file_name}") self._info = VolumeMeshMeta(**resp) @classmethod def _interface(cls): return VolumeMeshInterface @classmethod def _meta_class(cls): """ returns volume mesh meta info class: VolumeMeshMeta """ return VolumeMeshMeta @classmethod def _params_ancestor_id_name(cls): """ returns surfaceMeshId name """ return "surfaceMeshId" @classmethod def from_cloud(cls, mesh_id: str): """ Get volume mesh info from cloud :param mesh_id: :return: """ return cls(mesh_id) def _remote_file_name(self): """ mesh filename on cloud """ remote_file_name = None for file in self.get_download_file_list(): try: MeshNameParser(file["fileName"]) remote_file_name = file["fileName"] except Flow360RuntimeError: continue if remote_file_name is None: raise Flow360CloudFileError(f"No volume mesh file found for id={self.id}") return remote_file_name @classmethod def from_file( cls, file_name: str, params: Union[Flow360MeshParams, None] = None, name: str = None, tags: List[str] = None, solver_version=None, endianess: UGRIDEndianness = None, isascii: bool = False, ) -> VolumeMeshDraft: """ Upload volume mesh from file :param volume_mesh_name: :param file_name: :param params: :param tags: :param solver_version: :return: """ return VolumeMeshDraft( file_name=file_name, name=name, tags=tags, solver_version=solver_version, params=params, endianess=endianess, isascii=isascii, ) @classmethod def copy_from_example( cls, example_id: str, name: str = None, ) -> VolumeMesh: """ Create a new volume mesh by copying from an example mesh identified by `example_id`. Parameters ---------- example_id : str The unique identifier of the example volume mesh to copy from. name : str, optional The name to assign to the new volume mesh. If not provided, the name of the example volume mesh will be used. Returns ------- VolumeMesh A new instance of VolumeMesh copied from the example mesh if successful. Examples -------- >>> new_mesh = VolumeMesh.copy_from_example('example_id_123', name='New Mesh') """ if name is None: eg_vm = cls(example_id) name = eg_vm.name req = CopyExampleVolumeMeshRequest(example_id=example_id, name=name) resp = RestApi(f"{VolumeMeshInterface.endpoint}/examples/copy").post(req.dict()) if not resp: raise RuntimeError("Something went wrong when accessing example mesh.") info = VolumeMeshMeta(**resp) return cls(info.id) @classmethod def create( cls, name: str, params: VolumeMeshingParams, surface_mesh_id: str, tags: List[str] = None, solver_version=None, ) -> VolumeMeshDraft: """ Create volume mesh from surface mesh """ return VolumeMeshDraft( name=name, surface_mesh_id=surface_mesh_id, solver_version=solver_version, params=params, tags=tags, ) def create_case( self, name: str, params: Flow360Params, tags: List[str] = None, solver_version: str = None, ) -> CaseDraft: """ Create new case :param name: :param params: :param tags: :return: """ return Case.create( name, params, volume_mesh_id=self.id, tags=tags, solver_version=solver_version )
class VolumeMeshList(Flow360ResourceListBase): """ VolumeMesh List component """ def __init__( self, surface_mesh_id: str = None, from_cloud: bool = True, include_deleted: bool = False, limit=100, ): super().__init__( ancestor_id=surface_mesh_id, from_cloud=from_cloud, include_deleted=include_deleted, limit=limit, resourceClass=VolumeMesh, ) 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) -> VolumeMesh: """ returns VolumeMeshMeta item of the list """ return super().__getitem__(index) # pylint: disable=useless-parent-delegation def __iter__(self) -> Iterator[VolumeMesh]: return super().__iter__() ###==== V2 API version ===### class VolumeMeshStatusV2(Enum): """Status of volume mesh resource, the is_final method is overloaded""" SUBMITTED = "submitted" UPLOADING = "uploading" UPLOADED = "uploaded" COMPLETED = "completed" PENDING = "pending" GENERATING = "generating" ERROR = "error" def is_final(self): """ Checks if status is final for volume mesh resource Returns ------- bool True if status is final, False otherwise. """ if self in [VolumeMeshStatusV2.COMPLETED, VolumeMeshStatusV2.ERROR]: return True return False class VolumeMeshMetaV2(AssetMetaBaseModelV2): """ VolumeMeshMetaV2 component """ status: VolumeMeshStatusV2 = pd_v2.Field() # Overshadowing to ensure correct is_final() method file_name: Optional[str] = pd_v2.Field(None, alias="fileName") class VolumeMeshStats(pd_v2.BaseModel): """ Mesh stats """ n_nodes: int = pd_v2.Field(..., alias="nNodes") n_triangles: int = pd_v2.Field(..., alias="nTriangles") n_quadrilaterals: int = pd_v2.Field(..., alias="nQuadrilaterals") n_tetrahedron: int = pd_v2.Field(..., alias="nTetrahedron") n_prism: int = pd_v2.Field(..., alias="nPrism") n_pyramid: int = pd_v2.Field(..., alias="nPyramid") n_hexahedron: int = pd_v2.Field(..., alias="nHexahedron") n_tet_wedge: int = pd_v2.Field(..., alias="nTetWedge") class VolumeMeshBoundingBox(PerEntityResultCSVModel): """ VolumeMeshBoundingBox """ remote_file_name: str = pd.Field(VolumeMeshDownloadable.BOUNDING_BOX.value, frozen=True) _variables: Optional[List[str]] = None @property def entities(self): """ Returns list of entities (boundary names) available for this result """ return self.values.keys() def _filtered_sum(self): pass def _get_range(self, df, min_key: str, max_key: str) -> float: if min_key not in df.index or max_key not in df.index: return 0.0 min_val = df.loc[min_key].min() max_val = df.loc[max_key].max() return max_val - min_val @property def length(self) -> float: """ Compute and return the length of the bounding box. The length is calculated as the difference between the maximum and minimum x-coordinate values from the bounding box data. Returns: float: The computed length along the x-axis. """ df = self.as_dataframe() return self._get_range(df, "xmin", "xmax") @property def width(self) -> float: """ Compute and return the width of the bounding box. The width is calculated as the difference between the maximum and minimum y-coordinate values from the bounding box data. Returns: float: The computed width along the y-axis. """ df = self.as_dataframe() return self._get_range(df, "ymin", "ymax") @property def height(self) -> float: """ Compute and return the height of the bounding box. The height is calculated as the difference between the maximum and minimum z-coordinate values from the bounding box data. Returns: float: The computed height along the z-axis. """ df = self.as_dataframe() return self._get_range(df, "zmin", "zmax") class VolumeMeshDraftV2(ResourceDraft): """ Volume mesh draft component """ # pylint: disable=too-many-arguments def __init__( self, file_names: str, project_name: str = None, solver_version: str = None, length_unit: LengthUnitType = "m", tags: List[str] = None, folder: Optional[Folder] = None, ): self.file_name = file_names self.project_name = project_name self.tags = tags if tags is not None else [] self.length_unit = length_unit self.solver_version = solver_version self.folder = folder self._validate() ResourceDraft.__init__(self) def _validate(self): self._validate_volume_mesh() def _validate_volume_mesh(self): if self.file_name is not None: try: VolumeMeshFile(file_names=self.file_name) except pd.ValidationError as e: raise Flow360FileError(str(e)) from e if self.project_name is None: self.project_name = os.path.splitext(os.path.basename(self.file_name))[0] log.warning( "`project_name` is not provided. " f"Using the volume mesh file name {self.project_name} as project name." ) if self.length_unit not in LengthUnitType.__args__: raise Flow360ValueError( f"specified length_unit : {self.length_unit} is invalid. " f"Valid options are: {list(LengthUnitType.__args__)}" ) if self.solver_version is None: raise Flow360ValueError("solver_version field is required.") # pylint: disable=protected-access, too-many-locals def submit( self, description="", progress_callback=None, compress=True, run_async=False ) -> VolumeMeshV2: """ Submit volume mesh to cloud and create a new project Parameters ---------- description : str, optional description of the project, by default "" progress_callback : callback, optional Use for custom progress bar, by default None compress : boolean, optional Compress the volume mesh file when sending to S3, default is True fetch_entities : boolean, optional Whether to fetch and populate the entity info object after submitting, default is False run_async : bool, optional Whether to submit volume mesh asynchronously (default is False). Returns ------- VolumeMeshV2 Volume mesh object with id """ self._validate() if not shared_account_confirm_proceed(): raise Flow360ValueError("User aborted resource submit.") mesh_parser = MeshNameParser(self.file_name) original_compression = mesh_parser.compression mesh_format = mesh_parser.format file_name_no_compression = mesh_parser.file_name_no_compression compression = original_compression do_compression = False if compress and original_compression == CompressionFormat.NONE: compression = CompressionFormat.ZST do_compression = True original_file_with_compression = f"{file_name_no_compression}{compression.ext()}" req = NewVolumeMeshRequestV2( name=self.project_name, solver_version=self.solver_version, tags=self.tags, file_name=original_file_with_compression, parent_folder_id=self.folder.id if self.folder else "ROOT.FLOW360", length_unit=self.length_unit, format=mesh_format.value, description=description, ) # Create new volume mesh resource and project req_dict = req.dict() resp = RestApi(VolumeMeshInterfaceV2.endpoint).post(req_dict) info = VolumeMeshMetaV2(**resp) # setting _id will disable "WARNING: You have not submitted..." warning message self._id = info.id renamed_file_on_remote = info.file_name volume_mesh = VolumeMeshV2(info.id) # Upload volume mesh file, keep posting the heartbeat to keep server patient about uploading. heartbeat_info = {"resourceId": info.id, "resourceType": "VolumeMesh", "stop": False} heartbeat_thread = threading.Thread(target=post_upload_heartbeat, args=(heartbeat_info,)) heartbeat_thread.start() # Compress (if not already compressed) and upload if do_compression: zstd_compress(self.file_name, original_file_with_compression) volume_mesh._webapi._upload_file( renamed_file_on_remote, original_file_with_compression, progress_callback=progress_callback, ) os.remove(original_file_with_compression) else: volume_mesh._webapi._upload_file( renamed_file_on_remote, self.file_name, progress_callback=progress_callback ) if mesh_parser.is_ugrid(): expected_local_mapbc_file = mesh_parser.get_associated_mapbc_filename() if os.path.isfile(expected_local_mapbc_file): remote_mesh_parser = MeshNameParser(renamed_file_on_remote) volume_mesh._webapi._upload_file( remote_mesh_parser.get_associated_mapbc_filename(), mesh_parser.get_associated_mapbc_filename(), progress_callback=progress_callback, ) else: log.warning( f"The expected mapbc file {expected_local_mapbc_file} specifying " "user-specified boundary names doesn't exist." ) heartbeat_info["stop"] = True heartbeat_thread.join() # Start processing pipeline volume_mesh._webapi._complete_upload() log.info(f"VolumeMesh successfully submitted: {volume_mesh.short_description()}") self._id = info.id if run_async: return volume_mesh log.debug("Waiting for volume mesh to be processed.") volume_mesh._webapi.get_info() return VolumeMeshV2.from_cloud(volume_mesh.id)
[docs] class VolumeMeshV2(AssetBase): """ Volume mesh component for workbench (simulation V2) """ _interface_class = VolumeMeshInterfaceV2 _meta_class = VolumeMeshMetaV2 _draft_class = VolumeMeshDraftV2 _web_api_class = Flow360Resource _mesh_stats_file = "meshStats.json" _cloud_resource_type_name = "VolumeMesh"
[docs] @classmethod # pylint: disable=redefined-builtin def from_cloud(cls, id: str, **kwargs) -> VolumeMeshV2: """ Parameters ---------- id : str ID of the volume mesh resource in the cloud Returns ------- VolumeMeshV2 Volume mesh object """ asset_obj = super().from_cloud(id, **kwargs) return asset_obj
[docs] @classmethod def from_local_storage( cls, mesh_id: str = None, local_storage_path="", meta_data: VolumeMeshMetaV2 = None ) -> VolumeMeshV2: """ Parameters ---------- mesh_id : str ID of the volume mesh resource local_storage_path: The folder of the project, defaults to current working directory Returns ------- VolumeMeshV2 Volume mesh object """ return super()._from_local_storage( asset_id=mesh_id, local_storage_path=local_storage_path, meta_data=meta_data )
[docs] @classmethod # pylint: disable=too-many-arguments,arguments-renamed def from_file( cls, file_name: str, project_name: str = None, solver_version: str = None, length_unit: LengthUnitType = "m", tags: List[str] = None, folder: Optional[Folder] = None, ) -> VolumeMeshDraftV2: """ Parameters ---------- file_name : str The name of the input volume mesh file (``*.cgns``, ``*.ugrid``) project_name : str, optional The name of the newly created project, defaults to file name if empty solver_version: str Solver version to use for the project length_unit: LengthUnitType Length unit to use for the project ("m", "mm", "cm", "inch", "ft") tags: List[str] List of string tags to be added to the project upon creation folder : Optional[Folder], optional Parent folder for the project. If None, creates in root. Returns ------- VolumeMeshDraftV2 Draft of the volume mesh to be submitted """ # For type hint only but proper fix is to fully abstract the Draft class too. return super().from_file( file_names=file_name, project_name=project_name, solver_version=solver_version, length_unit=length_unit, tags=tags, folder=folder, )
# pylint: disable=useless-parent-delegation
[docs] def get_dynamic_default_settings(self, simulation_dict: dict): """Get the default volume mesh settings from the simulation dict""" return super().get_dynamic_default_settings(simulation_dict)
@cached_property def stats(self) -> VolumeMeshStats: """ Get mesh stats Returns ------- VolumeMeshStats return VolumeMeshStats object """ # pylint: disable=protected-access data = self._webapi._parse_json_from_cloud(self._mesh_stats_file) return VolumeMeshStats(**data) @cached_property def bounding_box(self) -> VolumeMeshBoundingBox: """ Get mesh bounding box Returns ------- VolumeMeshBoundingBox return VolumeMeshBoundingBox object """ # pylint: disable=protected-access data = self._webapi._parse_json_from_cloud(VolumeMeshDownloadable.BOUNDING_BOX.value) bbox = VolumeMeshBoundingBox.from_dict(data) return bbox @property def boundary_names(self) -> List[str]: """ Retrieve all boundary names available in this volume mesh as a list Returns ------- List[str] List of boundary names contained within the volume mesh """ self.internal_registry = self._entity_info.get_persistent_entity_registry( internal_registry=self.internal_registry ) # pylint: disable=protected-access return [surface.name for surface in self.internal_registry.view(Surface)._entities] @property def zone_names(self) -> List[str]: """ Retrieve all volume zone names available in this volume mesh as a list Returns ------- List[str] List of zone names contained within the volume mesh """ self.internal_registry = self._entity_info.get_persistent_entity_registry( internal_registry=self.internal_registry ) # pylint: disable=protected-access return [volume.name for volume in self.internal_registry.view(GenericVolume)._entities] def __getitem__(self, key: str): """ Parameters ---------- key : str The name of the entity to be found Returns ------- Surface The boundary object """ # pylint: disable=import-outside-toplevel from flow360.component.simulation.draft_context import get_active_draft if get_active_draft() is not None: log.warning( "Accessing entities via asset[key] while a DraftContext is active. " "Use draft.surfaces[key] or draft.volumes[key] instead to ensure " "modifications are tracked in the draft's entity_info." ) if isinstance(key, str) is False: raise Flow360ValueError(f"Entity naming pattern: {key} is not a string.") self.internal_registry = self._entity_info.get_persistent_entity_registry( internal_registry=self.internal_registry ) return self.internal_registry.find_by_naming_pattern( key, enforce_output_as_list=False, error_when_no_match=True ) def __setitem__(self, key: str, value: Any): raise NotImplementedError("Assigning/setting entities is not supported.")