-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathproject.py
More file actions
2455 lines (2163 loc) · 87.6 KB
/
project.py
File metadata and controls
2455 lines (2163 loc) · 87.6 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
"""Project interface for setting up and running simulations"""
# pylint: disable=no-member, too-many-lines
# To be honest I do not know why pylint is insistent on treating
# ProjectMeta instances as FieldInfo, I'd rather not have this line
from __future__ import annotations
import json
from enum import Enum
from typing import Dict, Iterable, List, Literal, Optional, Union
import pydantic as pd
import typing_extensions
from pydantic import PositiveInt
from flow360.cloud.file_cache import get_shared_cloud_file_cache
from flow360.cloud.flow360_requests import (
CloneVolumeMeshRequest,
LengthUnitType,
RenameAssetRequestV2,
)
from flow360.cloud.http_util import http
from flow360.cloud.rest_api import RestApi
from flow360.component.case import Case
from flow360.component.cloud_examples import (
copy_example,
fetch_examples,
find_example_by_name,
)
from flow360.component.geometry import Geometry
from flow360.component.interfaces import (
GeometryInterface,
ProjectInterface,
SurfaceMeshInterfaceV2,
VolumeMeshInterfaceV2,
)
from flow360.component.project_utils import (
apply_and_inform_grouping_selections,
deep_copy_entity_info,
load_status_from_asset,
set_up_params_for_uploading,
validate_params_with_context,
)
from flow360.component.resource_base import Flow360Resource
from flow360.component.simulation.draft_context.context import (
DraftContext,
get_active_draft,
)
from flow360.component.simulation.draft_context.coordinate_system_manager import (
CoordinateSystemStatus,
)
from flow360.component.simulation.draft_context.mirror import MirrorStatus
from flow360.component.simulation.draft_context.obb.tessellation_loader import (
TessellationFileLoader,
)
from flow360.component.simulation.entity_info import (
GeometryEntityInfo,
merge_geometry_entity_info,
)
from flow360.component.simulation.folder import Folder
from flow360.component.simulation.primitives import ImportedSurface
from flow360.component.simulation.simulation_params import SimulationParams
from flow360.component.simulation.unit_system import LengthType
from flow360.component.simulation.web.asset_base import AssetBase
from flow360.component.simulation.web.draft import Draft
from flow360.component.simulation.web.project_records import (
get_project_records,
show_projects_with_keyword_filter,
)
from flow360.component.simulation.web.utils import (
get_project_dependency_resource_metadata,
)
from flow360.component.surface_mesh_v2 import SurfaceMeshV2
from flow360.component.utils import (
AssetShortID,
GeometryFiles,
SurfaceMeshFile,
VolumeMeshFile,
formatting_validation_errors,
formatting_validation_warnings,
get_short_asset_id,
parse_datetime,
wrapstring,
)
from flow360.component.volume_mesh import VolumeMeshMetaV2, VolumeMeshV2
from flow360.exceptions import (
Flow360ConfigError,
Flow360FileError,
Flow360RuntimeError,
Flow360ValueError,
Flow360WebError,
)
from flow360.log import log
from flow360.version import __solver_version__
AssetOrResource = Union[type[AssetBase], type[Flow360Resource]]
class RootType(Enum):
"""
Enum for root object types in the project.
Attributes
----------
GEOMETRY : str
Represents a geometry root object.
SURFACE_MESH : str
Represents a surface mesh root object.
VOLUME_MESH : str
Represents a volume mesh root object.
"""
GEOMETRY = "Geometry"
SURFACE_MESH = "SurfaceMesh"
VOLUME_MESH = "VolumeMesh"
class ProjectDependencyType(Enum):
"""
Enum for dependency resource types in the project.
Attributes
----------
GEOMETRY : str
Represents a geometry dependency resource.
SURFACE_MESH : str
Represents a surface mesh dependency resource.
"""
GEOMETRY = "Geometry"
SURFACE_MESH = "SurfaceMesh"
# pylint: disable=too-many-arguments
def create_draft(
*,
new_run_from: Union[Geometry, SurfaceMeshV2, VolumeMeshV2],
face_grouping: Optional[str] = None,
edge_grouping: Optional[str] = None,
include_geometries: Optional[List[Geometry]] = None,
exclude_geometries: Optional[List[Geometry]] = None,
imported_surfaces: Optional[List[ImportedSurface]] = None,
) -> DraftContext:
"""
Create a local draft context from a cloud asset for your run.
A draft is an isolated, in-memory snapshot of an asset's entity information. It lets you
inspect and modify entities (surfaces, edges, volumes, body groups, etc.) locally without
mutating the cloud asset.
Draft allows you to:
- Override grouping tags for faces and edges (geometry assets only).
- Include or exclude geometry components (projects with a geometry root asset only).
- Register additional imported surfaces (surface mesh dependencies in the project).
- Access entities in the draft via `DraftContext` properties.
- Manage coordinate systems and mirror actions through `draft.coordinate_systems` and `draft.mirror`.
Parameters
----------
new_run_from : Union[Geometry, SurfaceMeshV2, VolumeMeshV2]
The cloud asset to create the draft from.
face_grouping : Optional[str]
Face grouping tag to activate for geometry assets. When None, the draft uses the
default grouping stored on the geometry asset. The tag must be one of the available
face grouping attributes (and, when multiple geometry components are active, must be
available across the activated components).
edge_grouping : Optional[str]
Edge grouping tag to activate for geometry assets. When None, the draft uses the
default grouping stored on the geometry asset. If the activated geometry does not
have edge grouping attributes, this option is ignored.
include_geometries : Optional[List[Geometry]]
Additional geometry components to activate in the draft (projects with a geometry
root asset only). The selected geometries are merged into the draft entity info.
exclude_geometries : Optional[List[Geometry]]
Geometry components to deactivate from the draft (projects with a geometry root asset
only). If a geometry is not currently active, its exclusion is ignored with a
warning.
imported_surfaces : Optional[List[ImportedSurface]]
Imported surface meshes to register in the draft. This is commonly obtained from
`Project.imported_surfaces` or created via `Project.import_surface_mesh(...)`.
Returns
-------
DraftContext
A draft context manager. Use it with `with` to set it as the active draft context.
Raises
------
Flow360RuntimeError
If `new_run_from` is not a cloud asset instance.
Flow360ValueError
If draft creation is attempted from a `Case` or an imported `Geometry`, if geometry
components are requested for a non-geometry-root project, or if an invalid grouping
tag is specified.
Example
-------
>>> import flow360 as fl
>>> geometry = fl.Geometry.from_cloud(id="...")
>>> with fl.create_draft(new_run_from=geometry, face_grouping="groupByName") as draft:
... print(draft.surfaces["wing*"])
"""
# region -----------------------------Private implementations Below-----------------------------
def _resolve_active_geometry_dependencies(
current_geometry_dependencies: List,
include_geometries: List[Geometry],
exclude_geometries: List[Geometry],
) -> Dict[str, Geometry]:
active_geometry_dependencies = {
geometry_dependency["id"]: Geometry.from_cloud(geometry_dependency["id"])
for geometry_dependency in current_geometry_dependencies
}
for geometry in include_geometries:
if geometry.id not in active_geometry_dependencies:
active_geometry_dependencies[geometry.id] = geometry
for geometry in exclude_geometries:
excluded_geometry = active_geometry_dependencies.pop(geometry.id, None)
if excluded_geometry is None:
log.warning(
f"Geometry {geometry.name} not found among current dependencies. Ignoring its exclusion."
)
return active_geometry_dependencies
def _merge_geometry_entity_info(
new_run_from, entity_info, active_geometry_dependencies: Dict[str, Geometry]
):
"""Merge the geometry entity info based on the root and imported geometries."""
if not active_geometry_dependencies:
return entity_info
# Add root geometry to components to be merged
project = Project.from_cloud(new_run_from.info.project_id)
root_geometry = project.geometry
active_geometry_dependencies.update({root_geometry.id: root_geometry})
merged_entity_info = merge_geometry_entity_info(
current_entity_info=entity_info,
entity_info_components=[
geometry.entity_info for geometry in active_geometry_dependencies.values()
],
)
return merged_entity_info
# endregion ------------------------------------------------------------------------------------
if not isinstance(new_run_from, AssetBase):
raise Flow360RuntimeError("create_draft expects a cloud asset instance as `new_run_from`.")
if isinstance(new_run_from, Geometry) and new_run_from.info.dependency:
raise Flow360ValueError("Draft creation from an imported Geometry is not supported.")
if isinstance(new_run_from, Case):
raise Flow360ValueError("Draft creation from a Case is not supported.")
if not isinstance(new_run_from.entity_info, GeometryEntityInfo) and (
include_geometries or exclude_geometries
):
raise Flow360ValueError(
"Only project with a geometry root asset supports editing geometry components."
"Please use create_draft without `include_geometries` and `exclude_geometries`."
)
# Deep copy entity_info for draft isolation
entity_info_copy = deep_copy_entity_info(new_run_from.entity_info)
# Resolve geometry components and merge entity info if applicable
active_geometry_dependencies = {}
if isinstance(new_run_from.entity_info, GeometryEntityInfo):
active_geometry_dependencies = _resolve_active_geometry_dependencies(
current_geometry_dependencies=new_run_from.info.geometry_dependencies or [],
include_geometries=include_geometries or [],
exclude_geometries=exclude_geometries or [],
)
entity_info_copy = _merge_geometry_entity_info(
new_run_from=new_run_from,
entity_info=entity_info_copy,
active_geometry_dependencies=active_geometry_dependencies,
)
apply_and_inform_grouping_selections(
entity_info=entity_info_copy,
face_grouping=face_grouping,
edge_grouping=edge_grouping,
new_run_from_geometry=isinstance(new_run_from, Geometry),
)
mirror_status = load_status_from_asset(
asset=new_run_from,
status_class=MirrorStatus,
cache_key="mirror_status",
)
coordinate_system_status = load_status_from_asset(
asset=new_run_from,
status_class=CoordinateSystemStatus,
cache_key="coordinate_system_status",
)
# Build tessellation loader for geometry-root drafts (enables compute_obb)
tessellation_loader = None
length_unit = None
if isinstance(new_run_from, Geometry):
# pylint: disable=protected-access
geometry_resources: Dict[str, Flow360Resource] = {new_run_from.id: new_run_from._webapi}
geometry_resources.update(
{geo.id: geo._webapi for geo in active_geometry_dependencies.values()}
)
tessellation_loader = TessellationFileLoader(
geometry_resources, get_shared_cloud_file_cache()
)
# Use length unit cached on Geometry during from_cloud (no extra API call)
length_unit = new_run_from._project_length_unit
return DraftContext(
entity_info=entity_info_copy,
mirror_status=mirror_status,
coordinate_system_status=coordinate_system_status,
imported_surfaces=imported_surfaces,
imported_geometries=list(active_geometry_dependencies.values()),
tessellation_loader=tessellation_loader,
length_unit=length_unit,
)
class ProjectMeta(pd.BaseModel, extra="allow"):
"""
Metadata class for a project.
Attributes
----------
user_id : str
The user ID associated with the project.
id : str
The project ID.
name : str
The name of the project.
tags : List[str]
List of tags associated with the project.
root_item_id : str
ID of the root item in the project.
root_item_type : RootType
Type of the root item (Geometry or SurfaceMesh or VolumeMesh).
"""
user_id: str = pd.Field(alias="userId")
id: str = pd.Field()
name: str = pd.Field()
tags: List[str] = pd.Field(default_factory=list)
root_item_id: str = pd.Field(alias="rootItemId")
root_item_type: RootType = pd.Field(alias="rootItemType")
class ProjectTreeNode(pd.BaseModel):
"""
ProjectTreeNode class containing the info of an asset item in a project tree.
Attributes
----------
asset_id : str
ID of the asset.
asset_name : str
Name of the asset.
asset_type : str
Type of the asset.
parent_id : Optional[str]
ID of the parent asset.
case_mesh_id : Optional[str]
ID of the case's mesh.
case_mesh_label : Optional[str]
Label the mesh of a forked case using a different mesh.
children : List
List of the child assets of the current asset.
min_length_short_id : int
The minimum length of the short asset id, excluding
hyphen and asset prefix.
"""
asset_id: str = pd.Field()
asset_name: str = pd.Field()
asset_type: str = pd.Field()
parent_id: Optional[str] = pd.Field(None)
case_mesh_id: Optional[str] = pd.Field(None)
case_mesh_label: Optional[str] = pd.Field(None)
children: List = pd.Field([])
min_length_short_id: PositiveInt = pd.Field(7)
def construct_string(self, line_width):
"""Define the output info within when printing a project tree in the terminal"""
title_line = "<<" + self.asset_type + ">>"
name_line = f"name: {self.asset_name}"
id_line = f"id: {self.short_id}"
# Dynamically compute the line_width for each asset block to ensure
# 1. The asset type title always occupies a single line
# 2. The id and name line width is no more than the input line_width but is as small as possible.
max_line_width = min(line_width, max(len(name_line), len(id_line)))
block_line_width = max(len(title_line), max_line_width)
name_line = wrapstring(long_str=f"name: {self.asset_name}", str_length=block_line_width)
id_line = wrapstring(long_str=f"id: {self.short_id}", str_length=block_line_width)
return f"{title_line.center(block_line_width)}\n{name_line}\n{id_line}"
def add_child(self, child: ProjectTreeNode):
"""Add a child asset of the current asset"""
self.children.append(child)
def remove_child(self, child_to_remove: ProjectTreeNode):
"""Remove a child asset of the current asset"""
self.children = [child for child in self.children if child is not child_to_remove]
@property
def short_id(self) -> str:
"""Compute short asset id"""
return get_short_asset_id(
full_asset_id=self.asset_id, num_character=self.min_length_short_id
)
@property
def edge_label(self) -> str:
"""
Add edge label in the printed project tree to
display the different volume mesh used in a forked case.
"""
if self.case_mesh_label:
prefix = "Using VolumeMesh:\n"
mesh_short_id = get_short_asset_id(
full_asset_id=self.case_mesh_label,
num_character=self.min_length_short_id,
)
return prefix + mesh_short_id.center(len(prefix))
return None
class ProjectTree(pd.BaseModel):
"""
ProjectTree class containing the project tree.
Attributes
----------
root : ProjectTreeNode
Root item of the project.
nodes : dict[str, ProjectTreeNode]
Dict of all nodes in the project tree.
short_id_map: dict[str, List[str]]
Dict of short_id to full_id mapping, used to ensure every short_id is unique in the project.
"""
root: ProjectTreeNode = pd.Field(None)
nodes: dict[str, ProjectTreeNode] = pd.Field({})
short_id_map: dict[str, List[str]] = pd.Field({})
def _update_case_mesh_label(self):
"""Check and remove unnecessary case mesh label"""
for node_id in self._get_asset_ids_by_type(asset_type="Case"):
node = self.nodes.get(node_id)
parent_node = self._get_parent_node(node=node)
if not parent_node:
continue
if parent_node.asset_type != "Case" or node.case_mesh_id == parent_node.case_mesh_id:
node.case_mesh_label = None
def _update_node_short_id(self):
"""Update the minimum length of short ID to ensure each node has a unique short ID"""
if len(self.nodes) == len(self.short_id_map):
pass
full_id_to_update = []
short_id_duplicate = []
for short_id, full_ids in self.short_id_map.items():
if len(full_ids) > 1:
short_id_duplicate.append(short_id)
common_prefix = full_ids[0]
for full_id in full_ids[1:]:
while not full_id.startswith(common_prefix):
common_prefix = common_prefix[:-1]
common_prefix_processed = "".join(common_prefix.split("-")[1:])
for full_id in full_ids:
# pylint: disable=unsubscriptable-object
self.nodes[full_id].min_length_short_id = len(common_prefix_processed) + 1
full_id_to_update.append(full_id)
for full_id in full_id_to_update:
# pylint: disable=unsubscriptable-object
self.short_id_map.update({self.nodes[full_id].short_id: [full_id]})
for short_id in short_id_duplicate:
self.short_id_map.pop(short_id, None)
def _get_parent_node(self, node: ProjectTreeNode):
"""Get the parent node of the input node"""
if not node.parent_id:
return None
return self.nodes.get(node.parent_id, None)
def _has_node(self, asset_id: str) -> bool:
"""Use asset_id to check if the asset already exists in the project tree"""
if asset_id in self.nodes.keys():
return True
return False
def _get_asset_ids_by_type(
self, asset_type: str = Literal["Geometry", "SurfaceMesh", "VolumeMesh", "Case"]
):
"""Get the list of asset_ids in the project tree by asset_type."""
return [node.asset_id for node in self.nodes.values() if node.asset_type == asset_type]
@classmethod
def _create_new_node(cls, asset_record: dict):
"""Create a new node based on the asset record from API call"""
parent_id = (
asset_record["parentCaseId"]
if asset_record["parentCaseId"]
else asset_record["parentId"]
)
case_mesh_id = asset_record["parentId"] if asset_record["type"] == "Case" else None
new_node = ProjectTreeNode(
asset_id=asset_record["id"],
asset_name=asset_record["name"],
asset_type=asset_record["type"],
parent_id=parent_id,
case_mesh_id=case_mesh_id,
case_mesh_label=case_mesh_id,
)
return new_node
def _update_short_id_map(self, new_node: ProjectTreeNode):
# pylint: disable=unsupported-assignment-operation,unsubscriptable-object
if new_node.short_id not in self.short_id_map.keys():
self.short_id_map[new_node.short_id] = []
self.short_id_map[new_node.short_id].append(new_node.asset_id)
def add(self, asset_record: dict):
"""Add new node to the existing project tree"""
if self._has_node(asset_id=asset_record["id"]):
return False
new_node = ProjectTree._create_new_node(asset_record)
self._update_short_id_map(new_node)
if new_node.parent_id is None:
self.root = new_node
for node in self.nodes.values():
if node.parent_id == new_node.asset_id:
new_node.add_child(child=node)
if node.asset_id == new_node.parent_id:
node.add_child(child=new_node)
self.nodes.update({new_node.asset_id: new_node})
self._update_node_short_id()
self._update_case_mesh_label()
return True
def remove_node(self, node_id: str):
"""Remove node from the tree"""
node = self.nodes.get(node_id)
if not node:
return
if node.parent_id and self._has_node(node.parent_id):
# pylint: disable=unsubscriptable-object
self.nodes[node.parent_id].remove_child(node)
self.nodes.pop(node.asset_id)
def construct_tree(self, asset_records: List[dict]):
"""Construct the entire project tree"""
for asset_record in asset_records:
new_node = ProjectTree._create_new_node(asset_record)
self._update_short_id_map(new_node)
if new_node.parent_id is None:
self.root = new_node
self.nodes.update({new_node.asset_id: new_node})
for node in self.nodes.values():
if node.parent_id and self._has_node(node.parent_id):
# pylint: disable=unsubscriptable-object
self.nodes[node.parent_id].add_child(node)
self._update_node_short_id()
self._update_case_mesh_label()
@pd.validate_call
def get_full_asset_id(self, query_asset: AssetShortID) -> str:
"""
Returns full asset id of a certain asset type given the query_id.
Raises
------
Flow360ValueError
1. If derived asset type from query_id does not match the asset type.
2. If query_id is too short.
3. If query_id is does not exist in the project tree.
Returns
-------
The full asset id.
"""
asset_type_ids = self._get_asset_ids_by_type(asset_type=query_asset.asset_type)
if len(asset_type_ids) == 0:
raise Flow360ValueError(f"No {query_asset.asset_type} is available in this project.")
if query_asset.asset_id is None:
# The latest asset of this asset_type will be returned.
return asset_type_ids[-1]
for asset_id in asset_type_ids:
if asset_id.startswith(query_asset.asset_id):
return asset_id
raise Flow360ValueError(
f"This asset does not exist in this project. Please check the input asset ID ({query_asset.asset_id})."
)
# pylint: disable=too-many-public-methods
class Project(pd.BaseModel):
"""
Project class containing the interface for creating and running simulations.
"""
metadata: ProjectMeta = pd.Field(description="Metadata of the project.")
project_tree: ProjectTree = pd.Field()
solver_version: str = pd.Field(frozen=True, description="Version of the solver being used.")
_root_asset: Union[Geometry, SurfaceMeshV2, VolumeMeshV2] = pd.PrivateAttr(None)
_root_webapi: Optional[RestApi] = pd.PrivateAttr(None)
_project_webapi: Optional[RestApi] = pd.PrivateAttr(None)
_root_simulation_json: Optional[dict] = pd.PrivateAttr(None)
@classmethod
def show_remote(cls, search_keyword: Union[None, str] = None):
"""
Shows all projects on the cloud.
Parameters
----------
search_keyword : str, optional
"""
show_projects_with_keyword_filter(search_keyword)
@property
def id(self) -> str:
"""
Returns the ID of the project.
Returns
-------
str
The project ID.
"""
return self.metadata.id
@property
def tags(self) -> List[str]:
"""
Returns the tags of the project.
Returns
-------
List[str]
List of the project's tags.
"""
return self.metadata.tags
@property
def length_unit(self) -> LengthType.Positive:
"""
Returns the length unit of the project.
Returns
-------
LengthType.Positive
The length unit.
"""
defaults = self._root_simulation_json
cache_key = "private_attribute_asset_cache"
length_key = "project_length_unit"
if cache_key not in defaults or length_key not in defaults[cache_key]:
raise Flow360ValueError("[Internal] Simulation params do not contain length unit info.")
return LengthType.validate(defaults[cache_key][length_key])
@property
def geometry(self) -> Geometry:
"""
Returns the geometry asset of the project. There is always only one geometry asset per project.
Raises
------
Flow360ValueError
If the geometry asset is not available for the project.
Returns
-------
Geometry
The geometry asset.
"""
self._check_initialized()
if self.metadata.root_item_type is not RootType.GEOMETRY:
raise Flow360ValueError(
"Geometry asset is only present in projects initialized from geometry."
)
return self._root_asset
def get_surface_mesh(self, asset_id: str = None) -> SurfaceMeshV2:
"""
Returns the surface mesh asset of the project.
Parameters
----------
asset_id : str, optional
The ID of the asset from among the generated assets in this project instance. If not provided,
the property contains the most recently run asset.
Raises
------
Flow360ValueError
If the surface mesh asset is not available for the project.
Returns
-------
SurfaceMeshV2
The surface mesh asset.
"""
self._check_initialized()
asset_id = self.project_tree.get_full_asset_id(
query_asset=AssetShortID(asset_id=asset_id, asset_type="SurfaceMesh")
)
return SurfaceMeshV2.from_cloud(id=asset_id)
@property
def surface_mesh(self) -> SurfaceMeshV2:
"""
Returns the last used surface mesh asset of the project.
If the project is initialized from surface mesh, the surface mesh asset is the root asset.
Raises
------
Flow360ValueError
If the surface mesh asset is not available for the project.
Returns
-------
SurfaceMeshV2
The surface mesh asset.
"""
if self.metadata.root_item_type is RootType.SURFACE_MESH:
return self._root_asset
log.warning(
f"Accessing surface mesh from a project initialized from {self.metadata.root_item_type.name}. "
"Please use the root asset for assigning entities to SimulationParams."
)
return self.get_surface_mesh()
def get_volume_mesh(self, asset_id: str = None) -> VolumeMeshV2:
"""
Returns the volume mesh asset of the project.
Parameters
----------
asset_id : str, optional
The ID of the asset from among the generated assets in this project instance. If not provided,
the property contains the most recently run asset.
Raises
------
Flow360ValueError
If the volume mesh asset is not available for the project.
Returns
-------
VolumeMeshV2
The volume mesh asset.
"""
self._check_initialized()
asset_id = self.project_tree.get_full_asset_id(
query_asset=AssetShortID(asset_id=asset_id, asset_type="VolumeMesh")
)
return VolumeMeshV2.from_cloud(id=asset_id)
@property
def volume_mesh(self) -> VolumeMeshV2:
"""
Returns the last used volume mesh asset of the project.
Raises
------
Flow360ValueError
If the volume mesh asset is not available for the project.
Returns
-------
VolumeMeshV2
The volume mesh asset.
"""
if self.metadata.root_item_type is RootType.VOLUME_MESH:
return self._root_asset
log.warning(
f"Accessing volume mesh from a project initialized from {self.metadata.root_item_type.name}. "
"Please use the root asset for assigning entities to SimulationParams."
)
return self.get_volume_mesh()
def get_case(self, asset_id: str = None) -> Case:
"""
Returns the last used case asset of the project.
Parameters
----------
asset_id : str, optional
The ID of the asset from among the generated assets in this project instance. If not provided,
the property contains the most recently run asset.
Raises
------
Flow360ValueError
If the case asset is not available for the project.
Returns
-------
Case
The case asset.
"""
self._check_initialized()
asset_id = self.project_tree.get_full_asset_id(
query_asset=AssetShortID(asset_id=asset_id, asset_type="Case")
)
return Case.from_cloud(case_id=asset_id)
@property
def case(self):
"""
Returns the case asset of the project.
Raises
------
Flow360ValueError
If the case asset is not available for the project.
Returns
-------
Case
The case asset.
"""
return self.get_case()
def get_surface_mesh_ids(self) -> Iterable[str]:
"""
Returns the available IDs of surface meshes in the project
Returns
-------
Iterable[str]
An iterable of asset IDs.
"""
# pylint: disable=protected-access
return self.project_tree._get_asset_ids_by_type(asset_type="SurfaceMesh")
def get_volume_mesh_ids(self):
"""
Returns the available IDs of volume meshes in the project
Returns
-------
Iterable[str]
An iterable of asset IDs.
"""
# pylint: disable=protected-access
return self.project_tree._get_asset_ids_by_type(asset_type="VolumeMesh")
def get_case_ids(self, tags: Optional[List[str]] = None) -> List[str]:
"""
Returns the available IDs of cases in the project, optionally filtered by tags.
Parameters
----------
tags : List[str], optional
List of tags to filter cases by. If None or empty tags list, returns all case IDs.
Returns
-------
Iterable[str]
An iterable of case IDs. If tags are provided, filters to return only
case IDs that have at least one matching tag.
"""
# pylint: disable=protected-access
all_case_ids = self.project_tree._get_asset_ids_by_type(asset_type="Case")
if not tags:
return all_case_ids
# Filter cases by tags
filtered_case_ids = []
for case_id in all_case_ids:
case = self.get_case(asset_id=case_id)
if set(tags) & set(case.info_v2.tags):
filtered_case_ids.append(case_id)
return filtered_case_ids
@classmethod
def get_project_ids(cls, tags: Optional[List[str]] = None) -> List[str]:
"""
Returns the available IDs of projects, optionally filtered by tags.
Parameters
----------
tags : List[str], optional
List of tags to filter projects by. If None, returns all project IDs.
Returns
-------
List[str]
A list of project IDs. If tags are provided, filters to return only
project IDs that have at least one matching tag.
"""
project_records, _ = get_project_records("", tags=tags)
return [record.project_id for record in project_records.records]
# pylint: disable=too-many-arguments
@classmethod
def _create_project_from_files(
cls,
*,
files: Union[GeometryFiles, SurfaceMeshFile, VolumeMeshFile],
name: str = None,
solver_version: str = __solver_version__,
length_unit: LengthUnitType = "m",
tags: List[str] = None,
run_async: bool = False,
folder: Optional[Folder] = None,
):
"""
Initializes a project from a file.
Parameters
----------
files : Union[GeometryFiles, SurfaceMeshFile, VolumeMeshFile]
Path to the files.
name : str, optional
Name of the project (default is None).
solver_version : str, optional
Version of the solver (default is None).
length_unit : LengthUnitType, optional
Unit of length (default is "m").
tags : list of str, optional
Tags to assign to the project (default is None).
run_async : bool, optional
Whether to create the project asynchronously (default is False).
folder : Optional[Folder], optional
Parent folder for the project. If None, creates in root.
Returns
-------
Project
An instance of the project. Or Project ID when run_async is True.
Raises
------
Flow360ValueError
If the project cannot be initialized from the file.
"""
root_asset = None
# pylint:disable = protected-access
files._check_files_existence()
if isinstance(files, GeometryFiles):
draft = Geometry.from_file(
files.file_names, name, solver_version, length_unit, tags, folder=folder
)
elif isinstance(files, SurfaceMeshFile):
draft = SurfaceMeshV2.from_file(
files.file_names, name, solver_version, length_unit, tags, folder=folder
)
elif isinstance(files, VolumeMeshFile):
draft = VolumeMeshV2.from_file(
files.file_names, name, solver_version, length_unit, tags, folder=folder
)
else:
raise Flow360FileError(
"Cannot detect the intended project root with the given file(s)."
)
root_asset = draft.submit(run_async=run_async)
if run_async:
log.info(
f"The input file(s) has been successfully uploaded to project: {root_asset.project_id} "
"and is being processed on cloud. Only the project ID string is returned. "
"To retrieve this project later, use 'Project.from_cloud(project_id)'. "
)
return root_asset.project_id
if not root_asset:
raise Flow360ValueError(f"Couldn't initialize asset from {files.file_names}")
project_id = root_asset.project_id
project_api = RestApi(ProjectInterface.endpoint, id=project_id)
info = project_api.get()