luminarycloud 0.17.0__py3-none-any.whl → 0.18.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- luminarycloud/__init__.py +4 -0
- luminarycloud/_client/client.py +3 -0
- luminarycloud/_helpers/_create_geometry.py +134 -32
- luminarycloud/_helpers/cond.py +0 -1
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +146 -123
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +81 -8
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.py +8 -8
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.pyi +12 -7
- luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2.py +246 -0
- luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2.pyi +420 -0
- luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2_grpc.py +240 -0
- luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2_grpc.pyi +90 -0
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.py +54 -3
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.pyi +92 -1
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2_grpc.py +132 -0
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2_grpc.pyi +40 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.py +88 -55
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.pyi +108 -1
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.py +35 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.pyi +16 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.py +48 -26
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.pyi +30 -2
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2_grpc.py +36 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2_grpc.pyi +18 -0
- luminarycloud/_proto/client/simulation_pb2.py +261 -251
- luminarycloud/_proto/client/simulation_pb2.pyi +40 -3
- luminarycloud/_proto/frontend/output/output_pb2.py +24 -24
- luminarycloud/_proto/frontend/output/output_pb2.pyi +6 -3
- luminarycloud/_proto/geometry/geometry_pb2.py +63 -63
- luminarycloud/_proto/geometry/geometry_pb2.pyi +14 -4
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.py +10 -10
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.pyi +12 -7
- luminarycloud/_proto/output/output_pb2.py +43 -36
- luminarycloud/_proto/output/output_pb2.pyi +28 -1
- luminarycloud/_proto/quantity/quantity_options_pb2.py +5 -4
- luminarycloud/_proto/quantity/quantity_options_pb2.pyi +4 -0
- luminarycloud/_proto/quantity/quantity_pb2.py +8 -8
- luminarycloud/enum/__init__.py +1 -0
- luminarycloud/enum/geometry_status.py +15 -8
- luminarycloud/enum/moment_convention_type.py +19 -0
- luminarycloud/enum/pipeline_job_status.py +23 -0
- luminarycloud/feature_modification.py +3 -1
- luminarycloud/geometry.py +12 -0
- luminarycloud/geometry_version.py +23 -0
- luminarycloud/outputs/output_definitions.py +5 -0
- luminarycloud/params/enum/_enum_wrappers.py +29 -0
- luminarycloud/params/simulation/monitor_plane_.py +1 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/fan_curve_inlet_.py +1 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/mach_inlet_.py +5 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/mass_flow_inlet_.py +5 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/total_pressure_inlet_.py +5 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/velocity_magnitude_inlet_.py +5 -1
- luminarycloud/physics_ai/inference.py +57 -30
- luminarycloud/pipelines/__init__.py +7 -0
- luminarycloud/pipelines/api.py +213 -0
- luminarycloud/pipelines/operators.py +4 -4
- luminarycloud/project.py +66 -6
- luminarycloud/simulation.py +6 -0
- luminarycloud/simulation_param.py +4 -2
- luminarycloud/simulation_queue.py +130 -0
- luminarycloud/simulation_template.py +21 -7
- luminarycloud/types/adfloat.py +3 -0
- luminarycloud/vis/__init__.py +4 -0
- luminarycloud/vis/interactive_inference.py +153 -0
- luminarycloud/vis/interactive_scene.py +49 -17
- luminarycloud/vis/primitives.py +9 -0
- luminarycloud/vis/visualization.py +22 -0
- luminarycloud/volume_selection.py +3 -2
- {luminarycloud-0.17.0.dist-info → luminarycloud-0.18.1.dist-info}/METADATA +18 -18
- {luminarycloud-0.17.0.dist-info → luminarycloud-0.18.1.dist-info}/RECORD +73 -64
- {luminarycloud-0.17.0.dist-info → luminarycloud-0.18.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Copyright 2023-2024 Luminary Cloud, Inc. All Rights Reserved.
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from luminarycloud._helpers import timestamp_to_datetime
|
|
7
|
+
|
|
8
|
+
from ..enum.pipeline_job_status import PipelineJobStatus
|
|
9
|
+
from ..pipelines import Pipeline, PipelineArgs
|
|
10
|
+
from .._client import get_default_client
|
|
11
|
+
from .._proto.api.v0.luminarycloud.pipelines import pipelines_pb2 as pipelinespb
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PipelineRecord:
|
|
16
|
+
id: str
|
|
17
|
+
name: str
|
|
18
|
+
description: str | None
|
|
19
|
+
definition_yaml: str
|
|
20
|
+
create_time: datetime
|
|
21
|
+
update_time: datetime
|
|
22
|
+
|
|
23
|
+
def pipeline(self) -> Pipeline:
|
|
24
|
+
return Pipeline._from_yaml(self.definition_yaml)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_proto(cls, proto: pipelinespb.Pipeline) -> "PipelineRecord":
|
|
28
|
+
return cls(
|
|
29
|
+
id=proto.id,
|
|
30
|
+
name=proto.name,
|
|
31
|
+
description=proto.description,
|
|
32
|
+
definition_yaml=proto.definition_yaml,
|
|
33
|
+
create_time=timestamp_to_datetime(proto.created_at),
|
|
34
|
+
update_time=timestamp_to_datetime(proto.updated_at),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class PipelineJobRecord:
|
|
40
|
+
id: str
|
|
41
|
+
pipeline_id: str
|
|
42
|
+
project_id: str
|
|
43
|
+
name: str
|
|
44
|
+
description: str | None
|
|
45
|
+
status: PipelineJobStatus
|
|
46
|
+
create_time: datetime
|
|
47
|
+
update_time: datetime
|
|
48
|
+
started_at: datetime | None
|
|
49
|
+
completed_at: datetime | None
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_proto(cls, proto: pipelinespb.PipelineJob) -> "PipelineJobRecord":
|
|
53
|
+
return cls(
|
|
54
|
+
id=proto.id,
|
|
55
|
+
pipeline_id=proto.pipeline_id,
|
|
56
|
+
project_id=proto.project_id,
|
|
57
|
+
name=proto.name,
|
|
58
|
+
description=proto.description,
|
|
59
|
+
status=PipelineJobStatus(proto.status),
|
|
60
|
+
create_time=timestamp_to_datetime(proto.created_at),
|
|
61
|
+
update_time=timestamp_to_datetime(proto.updated_at),
|
|
62
|
+
started_at=(
|
|
63
|
+
timestamp_to_datetime(proto.started_at) if proto.HasField("started_at") else None
|
|
64
|
+
),
|
|
65
|
+
completed_at=(
|
|
66
|
+
timestamp_to_datetime(proto.completed_at)
|
|
67
|
+
if proto.HasField("completed_at")
|
|
68
|
+
else None
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_pipeline(
|
|
74
|
+
name: str, pipeline: Pipeline | str, description: str | None = None
|
|
75
|
+
) -> PipelineRecord:
|
|
76
|
+
"""
|
|
77
|
+
Create a new pipeline.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
name : str
|
|
82
|
+
Name of the pipeline.
|
|
83
|
+
pipeline : Pipeline | str
|
|
84
|
+
The pipeline to create. Accepts a Pipeline object or a YAML-formatted pipeline definition.
|
|
85
|
+
description : str, optional
|
|
86
|
+
Description of the pipeline.
|
|
87
|
+
"""
|
|
88
|
+
if isinstance(pipeline, Pipeline):
|
|
89
|
+
definition_yaml = pipeline.to_yaml()
|
|
90
|
+
else:
|
|
91
|
+
definition_yaml = pipeline
|
|
92
|
+
req = pipelinespb.CreatePipelineRequest(
|
|
93
|
+
name=name, definition_yaml=definition_yaml, description=description
|
|
94
|
+
)
|
|
95
|
+
res: pipelinespb.CreatePipelineResponse = get_default_client().CreatePipeline(req)
|
|
96
|
+
return PipelineRecord.from_proto(res.pipeline)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def list_pipelines() -> list[PipelineRecord]:
|
|
100
|
+
"""
|
|
101
|
+
List all pipelines.
|
|
102
|
+
"""
|
|
103
|
+
req = pipelinespb.ListPipelinesRequest()
|
|
104
|
+
res: pipelinespb.ListPipelinesResponse = get_default_client().ListPipelines(req)
|
|
105
|
+
return [PipelineRecord.from_proto(p) for p in res.pipelines]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_pipeline(id: str) -> PipelineRecord:
|
|
109
|
+
"""
|
|
110
|
+
Get a pipeline by ID.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
id : str
|
|
115
|
+
ID of the pipeline to fetch.
|
|
116
|
+
"""
|
|
117
|
+
req = pipelinespb.GetPipelineRequest(id=id)
|
|
118
|
+
res: pipelinespb.GetPipelineResponse = get_default_client().GetPipeline(req)
|
|
119
|
+
return PipelineRecord.from_proto(res.pipeline)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def create_pipeline_job(
|
|
123
|
+
pipeline_id: str, args: PipelineArgs, project_id: str, name: str, description: str | None = None
|
|
124
|
+
) -> PipelineJobRecord:
|
|
125
|
+
"""
|
|
126
|
+
Create a new pipeline job.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
pipeline_id : str
|
|
131
|
+
ID of the pipeline to invoke.
|
|
132
|
+
args : PipelineArgs
|
|
133
|
+
Arguments to pass to the pipeline.
|
|
134
|
+
project_id : str
|
|
135
|
+
ID of the project to run the pipeline job in.
|
|
136
|
+
name : str
|
|
137
|
+
Name of the pipeline job.
|
|
138
|
+
description : str, optional
|
|
139
|
+
Description of the pipeline job.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
col_values = [[] for _ in args.params]
|
|
143
|
+
for row in args.rows:
|
|
144
|
+
for i, v in enumerate(row.row_values):
|
|
145
|
+
col_values[i].append(v)
|
|
146
|
+
|
|
147
|
+
cols = []
|
|
148
|
+
|
|
149
|
+
for i, param in enumerate(args.params):
|
|
150
|
+
if param._represented_type() == str:
|
|
151
|
+
cols.append(
|
|
152
|
+
pipelinespb.PipelineJobArgsColumn(
|
|
153
|
+
string_column=pipelinespb.PipelineJobArgsColumn.StringColumn(
|
|
154
|
+
name=param.name,
|
|
155
|
+
values=col_values[i],
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
elif param._represented_type() == int:
|
|
160
|
+
cols.append(
|
|
161
|
+
pipelinespb.PipelineJobArgsColumn(
|
|
162
|
+
int_column=pipelinespb.PipelineJobArgsColumn.IntColumn(
|
|
163
|
+
name=param.name,
|
|
164
|
+
values=col_values[i],
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
elif param._represented_type() == float:
|
|
169
|
+
cols.append(
|
|
170
|
+
pipelinespb.PipelineJobArgsColumn(
|
|
171
|
+
double_column=pipelinespb.PipelineJobArgsColumn.DoubleColumn(
|
|
172
|
+
name=param.name,
|
|
173
|
+
values=col_values[i],
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
elif param._represented_type() == bool:
|
|
178
|
+
cols.append(
|
|
179
|
+
pipelinespb.PipelineJobArgsColumn(
|
|
180
|
+
bool_column=pipelinespb.PipelineJobArgsColumn.BoolColumn(
|
|
181
|
+
name=param.name,
|
|
182
|
+
values=col_values[i],
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
req = pipelinespb.CreatePipelineJobRequest(
|
|
188
|
+
pipeline_id=pipeline_id,
|
|
189
|
+
args_columns=cols,
|
|
190
|
+
name=name,
|
|
191
|
+
description=description,
|
|
192
|
+
project_id=project_id,
|
|
193
|
+
)
|
|
194
|
+
res: pipelinespb.CreatePipelineJobResponse = get_default_client().CreatePipelineJob(req)
|
|
195
|
+
return PipelineJobRecord.from_proto(res.pipeline_job)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_pipeline_job(id: str) -> PipelineJobRecord:
|
|
199
|
+
"""
|
|
200
|
+
Get a pipeline job by ID.
|
|
201
|
+
"""
|
|
202
|
+
req = pipelinespb.GetPipelineJobRequest(id=id)
|
|
203
|
+
res: pipelinespb.GetPipelineJobResponse = get_default_client().GetPipelineJob(req)
|
|
204
|
+
return PipelineJobRecord.from_proto(res.pipeline_job)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def list_pipeline_jobs() -> list[PipelineJobRecord]:
|
|
208
|
+
"""
|
|
209
|
+
List all pipeline jobs.
|
|
210
|
+
"""
|
|
211
|
+
req = pipelinespb.ListPipelineJobsRequest()
|
|
212
|
+
res: pipelinespb.ListPipelineJobsResponse = get_default_client().ListPipelineJobs(req)
|
|
213
|
+
return [PipelineJobRecord.from_proto(p) for p in res.pipeline_jobs]
|
|
@@ -124,8 +124,8 @@ class Mesh(Operator[MeshOutputs]):
|
|
|
124
124
|
|
|
125
125
|
Parameters
|
|
126
126
|
----------
|
|
127
|
-
|
|
128
|
-
The
|
|
127
|
+
target_cv_count : int | None
|
|
128
|
+
The target number of control volumes to generate. If None, a minimal mesh will be generated.
|
|
129
129
|
geometry : PipelineOutputGeometry
|
|
130
130
|
The Geometry to mesh.
|
|
131
131
|
|
|
@@ -141,12 +141,12 @@ class Mesh(Operator[MeshOutputs]):
|
|
|
141
141
|
self,
|
|
142
142
|
*,
|
|
143
143
|
task_name: str | None = None,
|
|
144
|
-
|
|
144
|
+
target_cv_count: int | None,
|
|
145
145
|
geometry: PipelineOutputGeometry,
|
|
146
146
|
):
|
|
147
147
|
super().__init__(
|
|
148
148
|
task_name,
|
|
149
|
-
{"
|
|
149
|
+
{"target_cv_count": target_cv_count},
|
|
150
150
|
OperatorInputs(self, geometry=(PipelineOutputGeometry, geometry)),
|
|
151
151
|
MeshOutputs._instantiate_for(self),
|
|
152
152
|
)
|
luminarycloud/project.py
CHANGED
|
@@ -6,7 +6,7 @@ import re
|
|
|
6
6
|
import uuid
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from os import PathLike, path
|
|
9
|
-
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, Literal
|
|
10
10
|
|
|
11
11
|
import concurrent
|
|
12
12
|
|
|
@@ -121,22 +121,22 @@ class Project(ProtoWrapperBase):
|
|
|
121
121
|
|
|
122
122
|
def create_geometry(
|
|
123
123
|
self,
|
|
124
|
-
cad_file_path: PathLike | str,
|
|
124
|
+
cad_file_path: PathLike | str | List[PathLike | str],
|
|
125
125
|
*,
|
|
126
126
|
name: Optional[str] = None,
|
|
127
127
|
scaling: Optional[float] = None,
|
|
128
128
|
wait: bool = False,
|
|
129
129
|
) -> "Geometry":
|
|
130
130
|
"""
|
|
131
|
-
Create a new geometry in the project by uploading
|
|
131
|
+
Create a new geometry in the project by uploading supported CAD file(s).
|
|
132
132
|
|
|
133
133
|
For more information on supported formats and best practices, see:
|
|
134
134
|
https://docs.luminarycloud.com/en/articles/9274255-upload-cad
|
|
135
135
|
|
|
136
136
|
Parameters
|
|
137
137
|
----------
|
|
138
|
-
cad_file_path : PathLike
|
|
139
|
-
Path or URL to the CAD file to upload.
|
|
138
|
+
cad_file_path : PathLike | str | List[PathLike | str]
|
|
139
|
+
Path(s) or URL to the CAD file(s) to upload.
|
|
140
140
|
|
|
141
141
|
Other Parameters
|
|
142
142
|
----------------
|
|
@@ -662,6 +662,66 @@ class Project(ProtoWrapperBase):
|
|
|
662
662
|
return None
|
|
663
663
|
return get_named_variable_set(NamedVariableSetID(res.active_named_variable_set_id))
|
|
664
664
|
|
|
665
|
+
def share(self, email: str, role: Literal["viewer", "editor"]) -> None:
|
|
666
|
+
"""
|
|
667
|
+
Share the project with a user identified by their email address. This function also allows
|
|
668
|
+
changing the role of the user in the project if the project has already been shared with
|
|
669
|
+
the input user.
|
|
670
|
+
|
|
671
|
+
Parameters
|
|
672
|
+
----------
|
|
673
|
+
email : str
|
|
674
|
+
Email address of the user to share the project with. It must be an email whose domain
|
|
675
|
+
is registered within the allowed domains settings of your company account.
|
|
676
|
+
role : Literal["viewer", "editor"]
|
|
677
|
+
The role to assign to the user in the project. Must be either "viewer" or "editor".
|
|
678
|
+
"""
|
|
679
|
+
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
|
680
|
+
raise ValueError(f"Invalid email address: {email}")
|
|
681
|
+
if role not in ["viewer", "editor"]:
|
|
682
|
+
raise ValueError(f"Invalid role: {role}. Must be 'viewer' or 'editor'.")
|
|
683
|
+
roleModel = (
|
|
684
|
+
projectpb.ShareProjectRequest.USER_ROLE_VIEWER
|
|
685
|
+
if role == "viewer"
|
|
686
|
+
else projectpb.ShareProjectRequest.USER_ROLE_EDITOR
|
|
687
|
+
)
|
|
688
|
+
req = projectpb.ShareProjectRequest(id=self.id, email=email, role=roleModel)
|
|
689
|
+
get_default_client().ShareProject(req)
|
|
690
|
+
|
|
691
|
+
def unshare(self, email: str) -> None:
|
|
692
|
+
"""
|
|
693
|
+
Unshare the project with a user identified by their email address.
|
|
694
|
+
|
|
695
|
+
Parameters
|
|
696
|
+
----------
|
|
697
|
+
email : str
|
|
698
|
+
Email address of the user to unshare the project with. It must be an email whose domain
|
|
699
|
+
must be registered within the allowed domains settings of your company account.
|
|
700
|
+
"""
|
|
701
|
+
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
|
702
|
+
raise ValueError(f"Invalid email address: {email}")
|
|
703
|
+
req = projectpb.UnshareProjectRequest(id=self.id, email=email)
|
|
704
|
+
get_default_client().UnshareProject(req)
|
|
705
|
+
|
|
706
|
+
def share_with_support(self, message: str = "") -> None:
|
|
707
|
+
"""
|
|
708
|
+
Share the project with Luminary Cloud support.
|
|
709
|
+
|
|
710
|
+
Parameters
|
|
711
|
+
----------
|
|
712
|
+
message : str, optional
|
|
713
|
+
Message to include with the support share request.
|
|
714
|
+
"""
|
|
715
|
+
req = projectpb.ShareProjectWithSupportRequest(id=self.id, message=message)
|
|
716
|
+
get_default_client().ShareProjectWithSupport(req)
|
|
717
|
+
|
|
718
|
+
def unshare_with_support(self) -> None:
|
|
719
|
+
"""
|
|
720
|
+
Unshare the project with Luminary Cloud support.
|
|
721
|
+
"""
|
|
722
|
+
req = projectpb.UnshareProjectWithSupportRequest(id=self.id)
|
|
723
|
+
get_default_client().UnshareProjectWithSupport(req)
|
|
724
|
+
|
|
665
725
|
|
|
666
726
|
def add_named_variables_from_csv(project: Project, csv_path: str) -> list[NamedVariableSet]:
|
|
667
727
|
"""
|
|
@@ -838,6 +898,6 @@ def iterate_projects(page_size: int = 50) -> ProjectIterator:
|
|
|
838
898
|
Project(...)
|
|
839
899
|
>>> next(my_projects) # second page of projects is fetched, third project is returned
|
|
840
900
|
Project(...)
|
|
841
|
-
>>> next(my_projects) # if there
|
|
901
|
+
>>> next(my_projects) # if there are no more projects, this call raises StopIteration
|
|
842
902
|
"""
|
|
843
903
|
return ProjectIterator(page_size)
|
luminarycloud/simulation.py
CHANGED
|
@@ -18,6 +18,7 @@ from .enum import (
|
|
|
18
18
|
CalculationType,
|
|
19
19
|
QuantityType,
|
|
20
20
|
ResidualNormalization,
|
|
21
|
+
MomentConventionType,
|
|
21
22
|
SimulationStatus,
|
|
22
23
|
Vector3Component,
|
|
23
24
|
)
|
|
@@ -188,6 +189,7 @@ class Simulation(ProtoWrapperBase):
|
|
|
188
189
|
moment_center: Optional[Vector3Like] = None,
|
|
189
190
|
averaging_type: AveragingType = AveragingType.UNSPECIFIED,
|
|
190
191
|
vector_component: Vector3Component = Vector3Component.UNSPECIFIED,
|
|
192
|
+
moment_convention_type: MomentConventionType = MomentConventionType.BODY_FRAME,
|
|
191
193
|
) -> _DownloadedTextFile:
|
|
192
194
|
"""
|
|
193
195
|
Downloads surface outputs (e.g. lift, drag, ...) in csv format.
|
|
@@ -224,6 +226,9 @@ class Simulation(ProtoWrapperBase):
|
|
|
224
226
|
vector_component : Vector3Component, optional
|
|
225
227
|
For 3-vector quantity types (e.g. `QuantityType.VELOCITY`), the component of the vector to extract.
|
|
226
228
|
Ignored for scalar quantity types.
|
|
229
|
+
moment_convention_type : MomentConventionType, optional
|
|
230
|
+
The frame type to use for "aerodynamic moment" quantity types.
|
|
231
|
+
Ignored for non-moment quantity types.
|
|
227
232
|
|
|
228
233
|
Returns
|
|
229
234
|
-------
|
|
@@ -261,6 +266,7 @@ class Simulation(ProtoWrapperBase):
|
|
|
261
266
|
moment_center=_to_vector3_proto(moment_center) if moment_center else None,
|
|
262
267
|
averaging_type=averaging_type.value,
|
|
263
268
|
vector_component=vector_component.value,
|
|
269
|
+
moment_convention_type=moment_convention_type.value,
|
|
264
270
|
)
|
|
265
271
|
res = get_default_client().GetSimulationSurfaceQuantityOutput(req)
|
|
266
272
|
return _DownloadedTextFile(res.csv_file)
|
|
@@ -14,7 +14,7 @@ from luminarycloud._proto.client import simulation_pb2 as clientpb
|
|
|
14
14
|
from luminarycloud._proto.client.entity_pb2 import EntityIdentifier
|
|
15
15
|
from luminarycloud._proto.output import output_pb2 as outputpb
|
|
16
16
|
from luminarycloud._proto.quantity import quantity_options_pb2 as quantityoptspb
|
|
17
|
-
from luminarycloud.enum import AveragingType, QuantityType, SpaceAveragingType
|
|
17
|
+
from luminarycloud.enum import AveragingType, MomentConventionType, QuantityType, SpaceAveragingType
|
|
18
18
|
from luminarycloud.params.geometry import Volume
|
|
19
19
|
from luminarycloud.params.simulation import (
|
|
20
20
|
EntityRelationships,
|
|
@@ -265,6 +265,7 @@ class SimulationParam(_SimulationParam):
|
|
|
265
265
|
frame_id: str = "",
|
|
266
266
|
force_direction: Optional[Vector3Like] = None,
|
|
267
267
|
moment_center: Optional[Vector3Like] = None,
|
|
268
|
+
moment_convention_type: MomentConventionType = MomentConventionType.BODY_FRAME,
|
|
268
269
|
averaging_type: AveragingType = AveragingType.UNSPECIFIED,
|
|
269
270
|
) -> None:
|
|
270
271
|
"""
|
|
@@ -303,6 +304,7 @@ class SimulationParam(_SimulationParam):
|
|
|
303
304
|
_to_vector3_ad_proto(force_direction) if force_direction else None
|
|
304
305
|
),
|
|
305
306
|
moment_center=_to_vector3_ad_proto(moment_center) if moment_center else None,
|
|
307
|
+
moment_convention_type=moment_convention_type.value,
|
|
306
308
|
)
|
|
307
309
|
)
|
|
308
310
|
else:
|
|
@@ -329,7 +331,7 @@ class SimulationParam(_SimulationParam):
|
|
|
329
331
|
## code that creates (from scratch) an identical object. As such, some parts do
|
|
330
332
|
## not match 1 to 1 with hand-written examples.
|
|
331
333
|
import luminarycloud
|
|
332
|
-
from luminarycloud.types import Vector3
|
|
334
|
+
from luminarycloud.types import Vector3, Expression
|
|
333
335
|
from luminarycloud.tables import RectilinearTable
|
|
334
336
|
from luminarycloud.enum import *
|
|
335
337
|
from luminarycloud.params.enum import *
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Copyright 2025 Luminary Cloud, Inc. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
"""Simulation queue management functionality."""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ._client import get_default_client
|
|
9
|
+
from ._helpers._timestamp_to_datetime import timestamp_to_datetime
|
|
10
|
+
from ._proto.api.v0.luminarycloud.simulation import simulation_pb2 as simulationpb
|
|
11
|
+
from ._wrapper import ProtoWrapper, ProtoWrapperBase
|
|
12
|
+
from .types import ProjectID, SimulationID
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@ProtoWrapper(simulationpb.SimulationQueueStatus)
|
|
16
|
+
class SimulationQueueStatus(ProtoWrapperBase):
|
|
17
|
+
"""Represents the status of a queued simulation."""
|
|
18
|
+
|
|
19
|
+
project_id: ProjectID
|
|
20
|
+
"""The ID of the project to which the simulation belongs."""
|
|
21
|
+
simulation_id: SimulationID
|
|
22
|
+
"""The ID of the simulation."""
|
|
23
|
+
name: str
|
|
24
|
+
"""The name of the simulation."""
|
|
25
|
+
is_lma: bool
|
|
26
|
+
"""Whether this is an LMA simulation."""
|
|
27
|
+
priority: bool
|
|
28
|
+
"""Whether this is a priority simulation."""
|
|
29
|
+
|
|
30
|
+
_proto: simulationpb.SimulationQueueStatus
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def creation_time(self) -> datetime:
|
|
34
|
+
"""The time when the simulation was created."""
|
|
35
|
+
return timestamp_to_datetime(self._proto.creation_time)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def started_time(self) -> Optional[datetime]:
|
|
39
|
+
"""The time when the simulation started running, if it has started."""
|
|
40
|
+
if self._proto.HasField("started_time"):
|
|
41
|
+
return timestamp_to_datetime(self._proto.started_time)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SimulationStatusQueueIterator:
|
|
46
|
+
"""Iterator class for simulation status queue that provides length hint."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, page_size: int):
|
|
49
|
+
self._page_size: int = page_size
|
|
50
|
+
self._page_token: str = ""
|
|
51
|
+
self._total_count: Optional[int] = None
|
|
52
|
+
self._current_page: Optional[list[simulationpb.SimulationQueueStatus]] = None
|
|
53
|
+
self._client = get_default_client()
|
|
54
|
+
self._iterated_count: int = 0
|
|
55
|
+
|
|
56
|
+
def __iter__(self) -> "SimulationStatusQueueIterator":
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __next__(self) -> SimulationQueueStatus:
|
|
60
|
+
if self._current_page is None:
|
|
61
|
+
self._fetch_next_page()
|
|
62
|
+
|
|
63
|
+
# _current_page really can't be None here, but this assertion is needed to appease mypy
|
|
64
|
+
assert self._current_page is not None
|
|
65
|
+
|
|
66
|
+
if len(self._current_page) == 0:
|
|
67
|
+
if not self._page_token:
|
|
68
|
+
raise StopIteration
|
|
69
|
+
self._fetch_next_page()
|
|
70
|
+
|
|
71
|
+
self._iterated_count += 1
|
|
72
|
+
|
|
73
|
+
return SimulationQueueStatus(self._current_page.pop(0))
|
|
74
|
+
|
|
75
|
+
def _fetch_next_page(self) -> None:
|
|
76
|
+
req = simulationpb.ListQueuedSimulationsRequest(
|
|
77
|
+
page_size=self._page_size, page_token=self._page_token
|
|
78
|
+
)
|
|
79
|
+
res = self._client.ListQueuedSimulations(req)
|
|
80
|
+
|
|
81
|
+
self._current_page = list(res.simulations)
|
|
82
|
+
self._page_token = res.next_page_token
|
|
83
|
+
if self._total_count is None:
|
|
84
|
+
self._total_count = res.total_count or 0
|
|
85
|
+
|
|
86
|
+
def __length_hint__(self) -> int:
|
|
87
|
+
if self._total_count is None:
|
|
88
|
+
# Fetch first page to get total size if not already fetched
|
|
89
|
+
if self._current_page is None:
|
|
90
|
+
self._fetch_next_page()
|
|
91
|
+
return max(0, (self._total_count or 0) - self._iterated_count)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def iterate_simulation_status_queue(page_size: int = 50) -> SimulationStatusQueueIterator:
|
|
95
|
+
"""
|
|
96
|
+
Iterate over all simulations in the scheduling queue for the current account.
|
|
97
|
+
|
|
98
|
+
This function is only available for accounts with a Subscription Plan.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
page_size : int, optional
|
|
103
|
+
Number of simulations to fetch per page. Defaults to 50, max is 100.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
SimulationStatusQueueIterator
|
|
108
|
+
An iterator that yields SimulationQueueStatus objects one at a time.
|
|
109
|
+
|
|
110
|
+
Examples
|
|
111
|
+
--------
|
|
112
|
+
Fetch all queued simulations and filter them for LMA simulations.
|
|
113
|
+
|
|
114
|
+
>>> lma_sims = [sim for sim in iterate_simulation_status_queue() if sim.is_lma]
|
|
115
|
+
[SimulationQueueStatus(...), SimulationQueueStatus(...)]
|
|
116
|
+
|
|
117
|
+
Lazily fetch simulations.
|
|
118
|
+
(A batch size of 2 is a bad idea in real-world usage, but it helps demonstrate the lazy
|
|
119
|
+
fetching.)
|
|
120
|
+
|
|
121
|
+
>>> my_sims = iterate_simulation_status_queue(page_size=2)
|
|
122
|
+
>>> next(my_sims) # first page of simulations is fetched, first simulation is returned.
|
|
123
|
+
SimulationQueueStatus(...)
|
|
124
|
+
>>> next(my_sims) # second simulation is returned from memory.
|
|
125
|
+
SimulationQueueStatus(...)
|
|
126
|
+
>>> next(my_sims) # second page of simulations is fetched, third simulation is returned.
|
|
127
|
+
SimulationQueueStatus(...)
|
|
128
|
+
>>> next(my_sims) # if there are no more simulations, this call raises StopIteration.
|
|
129
|
+
"""
|
|
130
|
+
return SimulationStatusQueueIterator(page_size)
|
|
@@ -17,7 +17,7 @@ from ._proto.api.v0.luminarycloud.simulation_template import (
|
|
|
17
17
|
)
|
|
18
18
|
from ._proto.client import simulation_pb2 as clientpb
|
|
19
19
|
from ._wrapper import ProtoWrapper, ProtoWrapperBase
|
|
20
|
-
from .types import SimulationTemplateID
|
|
20
|
+
from .types import SimulationTemplateID, ProjectID
|
|
21
21
|
from .tables import RectilinearTable
|
|
22
22
|
from .simulation_param import SimulationParam
|
|
23
23
|
from .outputs import (
|
|
@@ -59,7 +59,9 @@ class SimulationTemplate(ProtoWrapperBase):
|
|
|
59
59
|
id: SimulationTemplateID
|
|
60
60
|
"Simulation template ID."
|
|
61
61
|
name: str
|
|
62
|
-
"Simulation name."
|
|
62
|
+
"Simulation template name."
|
|
63
|
+
project_id: ProjectID
|
|
64
|
+
"Project this simulation template belongs to."
|
|
63
65
|
|
|
64
66
|
_proto: simtemplatepb.SimulationTemplate
|
|
65
67
|
|
|
@@ -71,6 +73,13 @@ class SimulationTemplate(ProtoWrapperBase):
|
|
|
71
73
|
def update_time(self) -> datetime:
|
|
72
74
|
return timestamp_to_datetime(self._proto.update_time)
|
|
73
75
|
|
|
76
|
+
def sync_to_ui(self) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Sets this simulation template as the one that is used for the "Setup" tab in the UI.
|
|
79
|
+
"""
|
|
80
|
+
req = simtemplatepb.SyncSimulationTemplateToUIRequest(id=self.id)
|
|
81
|
+
get_default_client().SyncSimulationTemplateToUI(req)
|
|
82
|
+
|
|
74
83
|
def update(
|
|
75
84
|
self,
|
|
76
85
|
*,
|
|
@@ -480,10 +489,10 @@ class SimulationTemplate(ProtoWrapperBase):
|
|
|
480
489
|
code = code.replace("SimulationParam", "SimulationTemplate")
|
|
481
490
|
code += "\n\n\n"
|
|
482
491
|
code += '# Create a new simulation template or modify the one that is synced with the UI "Setup" tab.\n'
|
|
483
|
-
code +=
|
|
484
|
-
code +=
|
|
485
|
-
code +=
|
|
486
|
-
code += "template = project.list_simulation_templates()[0] # Setup template\n\n"
|
|
492
|
+
code += f'project = luminarycloud.get_project("{self.project_id}")\n'
|
|
493
|
+
code += f"template = project.create_simulation_template(name={self.name})\n"
|
|
494
|
+
code += '# TODO(USER): To modify the "Setup" template, uncomment the line below and comment out the line above.\n'
|
|
495
|
+
code += "# template = project.list_simulation_templates()[0] # Setup template\n\n"
|
|
487
496
|
|
|
488
497
|
if parameters._table_references:
|
|
489
498
|
code += "# Upload tabular data.\n"
|
|
@@ -509,6 +518,11 @@ class SimulationTemplate(ProtoWrapperBase):
|
|
|
509
518
|
for i, definition in enumerate(output_definitions):
|
|
510
519
|
if i == 0:
|
|
511
520
|
code += "output_list = []\n"
|
|
521
|
+
if isinstance(definition, DerivedOutputDefinition):
|
|
522
|
+
code += "# WARNING: Output {i} - Custom outputs are not yet supported in the SDK.\n"
|
|
523
|
+
# This is to make the stopping condition ID logic work.
|
|
524
|
+
code += "output_list.append(None)\n\n"
|
|
525
|
+
continue
|
|
512
526
|
output_code = definition._to_code_helper("new_output", hide_defaults)
|
|
513
527
|
for line in output_code.split("\n"):
|
|
514
528
|
# Omit ID because we are generating for create_output_definition.
|
|
@@ -526,7 +540,7 @@ class SimulationTemplate(ProtoWrapperBase):
|
|
|
526
540
|
code += "\n# Output-based conditions require the ID of the associated output.\n"
|
|
527
541
|
# Find the old output to use the new ID created by create_output_definition.
|
|
528
542
|
for j, od in enumerate(output_definitions):
|
|
529
|
-
if sc.output_definition_id == od.id:
|
|
543
|
+
if sc.output_definition_id == od.id and not isinstance(od, DerivedOutputDefinition):
|
|
530
544
|
code += f"template.create_or_update_stopping_condition(output_list[{j}].id, "
|
|
531
545
|
code += f"{sc.threshold}, {sc.start_at_iteration}, {sc.averaging_iterations}, "
|
|
532
546
|
code += f"{sc.iterations_to_consider})\n"
|
luminarycloud/types/adfloat.py
CHANGED
|
@@ -145,6 +145,9 @@ class Expression:
|
|
|
145
145
|
return False
|
|
146
146
|
return self._value == other._value and self._expression == other._expression
|
|
147
147
|
|
|
148
|
+
def _to_code(self, *args) -> str:
|
|
149
|
+
return f"Expression({self._expression.__repr__()})"
|
|
150
|
+
|
|
148
151
|
|
|
149
152
|
LcFloat = Union[float, FirstOrderAdFloat, SecondOrderAdFloat, Expression]
|
|
150
153
|
|