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.
Files changed (73) hide show
  1. luminarycloud/__init__.py +4 -0
  2. luminarycloud/_client/client.py +3 -0
  3. luminarycloud/_helpers/_create_geometry.py +134 -32
  4. luminarycloud/_helpers/cond.py +0 -1
  5. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +146 -123
  6. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +81 -8
  7. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.py +34 -0
  8. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.pyi +12 -0
  9. luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.py +8 -8
  10. luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.pyi +12 -7
  11. luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2.py +246 -0
  12. luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2.pyi +420 -0
  13. luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2_grpc.py +240 -0
  14. luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2_grpc.pyi +90 -0
  15. luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.py +54 -3
  16. luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.pyi +92 -1
  17. luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2_grpc.py +132 -0
  18. luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2_grpc.pyi +40 -0
  19. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.py +88 -55
  20. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.pyi +108 -1
  21. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.py +35 -0
  22. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.pyi +16 -0
  23. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.py +48 -26
  24. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.pyi +30 -2
  25. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2_grpc.py +36 -0
  26. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2_grpc.pyi +18 -0
  27. luminarycloud/_proto/client/simulation_pb2.py +261 -251
  28. luminarycloud/_proto/client/simulation_pb2.pyi +40 -3
  29. luminarycloud/_proto/frontend/output/output_pb2.py +24 -24
  30. luminarycloud/_proto/frontend/output/output_pb2.pyi +6 -3
  31. luminarycloud/_proto/geometry/geometry_pb2.py +63 -63
  32. luminarycloud/_proto/geometry/geometry_pb2.pyi +14 -4
  33. luminarycloud/_proto/inferenceservice/inferenceservice_pb2.py +10 -10
  34. luminarycloud/_proto/inferenceservice/inferenceservice_pb2.pyi +12 -7
  35. luminarycloud/_proto/output/output_pb2.py +43 -36
  36. luminarycloud/_proto/output/output_pb2.pyi +28 -1
  37. luminarycloud/_proto/quantity/quantity_options_pb2.py +5 -4
  38. luminarycloud/_proto/quantity/quantity_options_pb2.pyi +4 -0
  39. luminarycloud/_proto/quantity/quantity_pb2.py +8 -8
  40. luminarycloud/enum/__init__.py +1 -0
  41. luminarycloud/enum/geometry_status.py +15 -8
  42. luminarycloud/enum/moment_convention_type.py +19 -0
  43. luminarycloud/enum/pipeline_job_status.py +23 -0
  44. luminarycloud/feature_modification.py +3 -1
  45. luminarycloud/geometry.py +12 -0
  46. luminarycloud/geometry_version.py +23 -0
  47. luminarycloud/outputs/output_definitions.py +5 -0
  48. luminarycloud/params/enum/_enum_wrappers.py +29 -0
  49. luminarycloud/params/simulation/monitor_plane_.py +1 -1
  50. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/fan_curve_inlet_.py +1 -1
  51. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/mach_inlet_.py +5 -1
  52. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/mass_flow_inlet_.py +5 -1
  53. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/total_pressure_inlet_.py +5 -1
  54. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/velocity_magnitude_inlet_.py +5 -1
  55. luminarycloud/physics_ai/inference.py +57 -30
  56. luminarycloud/pipelines/__init__.py +7 -0
  57. luminarycloud/pipelines/api.py +213 -0
  58. luminarycloud/pipelines/operators.py +4 -4
  59. luminarycloud/project.py +66 -6
  60. luminarycloud/simulation.py +6 -0
  61. luminarycloud/simulation_param.py +4 -2
  62. luminarycloud/simulation_queue.py +130 -0
  63. luminarycloud/simulation_template.py +21 -7
  64. luminarycloud/types/adfloat.py +3 -0
  65. luminarycloud/vis/__init__.py +4 -0
  66. luminarycloud/vis/interactive_inference.py +153 -0
  67. luminarycloud/vis/interactive_scene.py +49 -17
  68. luminarycloud/vis/primitives.py +9 -0
  69. luminarycloud/vis/visualization.py +22 -0
  70. luminarycloud/volume_selection.py +3 -2
  71. {luminarycloud-0.17.0.dist-info → luminarycloud-0.18.1.dist-info}/METADATA +18 -18
  72. {luminarycloud-0.17.0.dist-info → luminarycloud-0.18.1.dist-info}/RECORD +73 -64
  73. {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
- max_cv_count : int
128
- The maximum number of control volumes to generate.
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
- max_cv_count: int,
144
+ target_cv_count: int | None,
145
145
  geometry: PipelineOutputGeometry,
146
146
  ):
147
147
  super().__init__(
148
148
  task_name,
149
- {"max_cv_count": max_cv_count},
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 a supported CAD file.
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 or str
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 areno more projects, this call raises StopIteration
901
+ >>> next(my_projects) # if there are no more projects, this call raises StopIteration
842
902
  """
843
903
  return ProjectIterator(page_size)
@@ -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 += "# TODO(USER): Replace ... with project ID, or create a project.\n"
484
- code += 'project = luminarycloud.get_project("...")\n'
485
- code += f"# template = project.create_simulation_template(name={self.name})\n"
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"
@@ -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
 
@@ -48,3 +48,7 @@ from .display import (
48
48
  from .interactive_scene import (
49
49
  InteractiveScene as InteractiveScene,
50
50
  )
51
+
52
+ from .interactive_inference import (
53
+ InteractiveInference as InteractiveInference,
54
+ )