Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions flow360/component/case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Surface mesh component
"""
import math

from pydantic import Extra, Field

from flow360.cloud.http_util import http
from flow360.component.flow360_base_model import Flow360BaseModel
from flow360.component.flow360_solver_params import Flow360Params


class Case(Flow360BaseModel, extra=Extra.allow):
"""
Case component
"""

case_id: str = Field(alias="caseId")
case_mesh_id: str = Field(alias="caseMeshId")
status: str = Field(alias="caseStatus")
parent_id: str = Field(alias="parentId")

@classmethod
def from_cloud(cls, case_id: str):
"""
Get case info from cloud
:param case_id:
:return:
"""
case = http.get(f"cases/{case_id}")
return cls(**case)

# pylint: disable=too-many-arguments
@classmethod
def submit_from_volume_mesh(
cls,
case_name: str,
volume_mesh_id: str,
params: Flow360Params,
tags: [str] = None,
parent_id=None,
):
"""
Create case from volume mesh
:param case_name:
:param volume_mesh_id:
:param params:
:param tags:
:param parent_id:
:return:
"""

assert case_name
assert volume_mesh_id
assert params

case = http.post(
f"volumemeshes/{volume_mesh_id}/case",
json={
"name": case_name,
"meshId": volume_mesh_id,
"runtimeParams": params.json(),
"tags": tags,
"parentId": parent_id,
},
)
return cls(**case)

# pylint: disable=too-many-arguments
@classmethod
def submit_multiple_phases(
cls,
case_name: str,
volume_mesh_id: str,
params: Flow360Params,
tags: [str] = None,
phase_steps=1,
):
"""
Create multiple cases from volume mesh
:param case_name:
:param volume_mesh_id:
:param params:
:param tags:
:param parent_id:
:param phase_steps:
:return:
"""

assert case_name
assert volume_mesh_id
assert params
assert phase_steps >= 1

result = []

total_steps = (
params.time_stepping.max_physical_steps
if params.time_stepping and params.time_stepping.max_physical_steps
else 1
)

num_cases = math.ceil(total_steps / phase_steps)
for i in range(1, num_cases + 1):
parent_id = result[-1].case_id if result else None
case = http.post(
f"volumemeshes/{volume_mesh_id}/case",
json={
"name": f"{case_name}_{i}",
"meshId": volume_mesh_id,
"runtimeParams": params.json(),
"tags": tags,
"parentId": parent_id,
},
)

result.append(cls(**case))

return result
40 changes: 31 additions & 9 deletions flow360/component/flow360_solver_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ class MeshBoundary(BaseModel):
no_slip_walls: Optional[Union[List[str], List[int]]] = Field(alias="noSlipWalls")


class Flow360MeshParams(BaseModel):
"""
Flow360 mesh parameters
"""

boundaries: MeshBoundary


class Boundary(BaseModel):
"""
Basic Boundary class
Expand All @@ -36,7 +28,7 @@ class NoSlipWall(Boundary):
"""

type = "NoSlipWall"
velocity: Union[float, str] = Field(alias="Velocity")
velocity: Optional[Union[float, str]] = Field(alias="Velocity")


class SlipWall(Boundary):
Expand All @@ -61,9 +53,39 @@ class ActuatorDisk(BaseModel):
force_per_area: Any = Field(alias="forcePerArea")


class SlidingInterface(BaseModel):
"""
Sliding interface component
"""

stationary_patches: Optional[List[str]] = Field(alias="stationaryPatches")
rotating_patches: Optional[List[str]] = Field(alias="rotatingPatches")
axis_of_rotation: Optional[List[int]] = Field(alias="axisOfRotation")
center_of_rotation: Optional[List[int]] = Field(alias="centerOfRotation")


class TimeStepping(BaseModel, extra=Extra.allow):
"""
Time stepping component
"""

max_physical_steps: Optional[int] = Field(alias="maxPhysicalSteps")


class Flow360Params(BaseModel, extra=Extra.allow):
"""
Flow360 solver parameters
"""

boundaries: Optional[Dict[str, BoundaryType]]
time_stepping: Optional[TimeStepping] = Field(alias="timeStepping")
sliding_interfaces: Optional[SlidingInterface] = Field(alias="slidingInterfaces")


class Flow360MeshParams(BaseModel):
"""
Flow360 mesh parameters
"""

boundaries: Optional[MeshBoundary]
sliding_interfaces: Optional[SlidingInterface] = Field(alias="slidingInterfaces")
196 changes: 195 additions & 1 deletion flow360/component/volume_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,162 @@
"""
import json
import os.path
import warnings
from enum import Enum
from typing import Optional, Union

import numpy as np
from pydantic import Extra, Field

from flow360.cloud.http_util import http
from flow360.cloud.s3_utils import S3TransferType
from flow360.component.flow360_base_model import Flow360BaseModel
from flow360.component.flow360_solver_params import Flow360MeshParams, Flow360Params
from flow360.component.flow360_solver_params import (
Flow360MeshParams,
Flow360Params,
NoSlipWall,
)
from flow360.version import Flow360Version

try:
import h5py

_H5PY_AVAILABLE = True
except ImportError:
_H5PY_AVAILABLE = False


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:
:param solver_version:
: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.items()
if isinstance(wall, NoSlipWall)
]

return []


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

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:
"""
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_boundries_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 ValueError(
"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)}"
)
print(
f'Notice: {" ".join(boundaries_in_file - boundaries_in_params)} is '
+ "tagged as wall in cgns file, but not in input params"
)


class VolumeMeshLog(Enum):
Expand Down Expand Up @@ -192,3 +339,50 @@ def from_ugrid_file(
if resp:
return cls(**resp)
return None

# pylint: disable=too-many-arguments

@classmethod
def from_cgns_file(
cls,
volume_mesh_name: str,
file_name: str,
params: Union[Flow360Params, Flow360MeshParams],
tags: [str] = None,
solver_version=None,
):
"""
Create volume mesh from ugrid file
:param volume_mesh_name:
:param file_name:
:param params:
:param tags:
:param solver_version:
:return:
"""
assert volume_mesh_name
assert os.path.exists(file_name)
assert params

if _H5PY_AVAILABLE:
validate_cgns(file_name, params, solver_version=solver_version)
else:
warnings.warn(
"Could not check consistency between mesh file and"
" Flow360.json file. h5py module not found. This is optional functionality"
)

body = {
"meshName": volume_mesh_name,
"meshTags": tags,
"meshFormat": "aflr3",
"meshParams": params.json(),
}

if solver_version:
body["solverVersion"] = solver_version

resp = http.post("volumemeshes", body)
if resp:
return cls(**resp)
return None
Loading