-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathvolume_mesh.py
More file actions
1274 lines (1083 loc) · 39.2 KB
/
volume_mesh.py
File metadata and controls
1274 lines (1083 loc) · 39.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
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.entity_info import VolumeMeshEntityInfo
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
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:
: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:
"""
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
)
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)