luminarycloud 0.19.0__py3-none-any.whl → 0.22.0__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 +5 -1
- luminarycloud/_client/client.py +7 -0
- luminarycloud/_client/http_client.py +10 -8
- luminarycloud/_feature_flag.py +22 -0
- luminarycloud/_helpers/_create_simulation.py +7 -2
- luminarycloud/_helpers/_upload_mesh.py +1 -0
- luminarycloud/_helpers/_wait_for_mesh.py +6 -5
- luminarycloud/_helpers/_wait_for_simulation.py +3 -3
- luminarycloud/_helpers/download.py +3 -1
- luminarycloud/_helpers/pagination.py +62 -0
- luminarycloud/_helpers/proto_decorator.py +13 -5
- luminarycloud/_helpers/upload.py +18 -12
- luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2.py +55 -0
- luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2.pyi +52 -0
- luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2_grpc.py +72 -0
- luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2_grpc.pyi +35 -0
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +168 -124
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +133 -4
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.py +66 -0
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.pyi +20 -0
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.py +8 -8
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.pyi +5 -5
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.py +74 -73
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.pyi +17 -3
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.py +96 -25
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.pyi +235 -1
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.py +16 -16
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.pyi +7 -3
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.py +97 -61
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.pyi +77 -4
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.py +33 -31
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.pyi +23 -2
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.py +126 -27
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.pyi +183 -0
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2_grpc.py +99 -0
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2_grpc.pyi +30 -0
- luminarycloud/_proto/assistant/assistant_pb2.py +74 -41
- luminarycloud/_proto/assistant/assistant_pb2.pyi +64 -2
- luminarycloud/_proto/assistant/assistant_pb2_grpc.py +33 -0
- luminarycloud/_proto/assistant/assistant_pb2_grpc.pyi +10 -0
- luminarycloud/_proto/base/base_pb2.py +20 -7
- luminarycloud/_proto/base/base_pb2.pyi +38 -0
- luminarycloud/_proto/cad/shape_pb2.py +39 -19
- luminarycloud/_proto/cad/shape_pb2.pyi +86 -34
- luminarycloud/_proto/cad/transformation_pb2.py +60 -16
- luminarycloud/_proto/cad/transformation_pb2.pyi +138 -32
- luminarycloud/_proto/client/simulation_pb2.py +501 -348
- luminarycloud/_proto/client/simulation_pb2.pyi +607 -11
- luminarycloud/_proto/geometry/geometry_pb2.py +77 -63
- luminarycloud/_proto/geometry/geometry_pb2.pyi +42 -3
- luminarycloud/_proto/hexmesh/hexmesh_pb2.py +24 -18
- luminarycloud/_proto/hexmesh/hexmesh_pb2.pyi +23 -2
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.py +10 -10
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.pyi +5 -5
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2.py +29 -0
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2.pyi +7 -0
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.py +70 -0
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.pyi +30 -0
- luminarycloud/_proto/quantity/quantity_options_pb2.py +6 -6
- luminarycloud/_proto/quantity/quantity_options_pb2.pyi +10 -1
- luminarycloud/_proto/quantity/quantity_pb2.py +176 -167
- luminarycloud/_proto/quantity/quantity_pb2.pyi +11 -5
- luminarycloud/enum/__init__.py +1 -0
- luminarycloud/enum/gpu_type.py +2 -0
- luminarycloud/enum/quantity_type.py +9 -0
- luminarycloud/enum/vis_enums.py +23 -3
- luminarycloud/exceptions.py +7 -1
- luminarycloud/feature_modification.py +45 -35
- luminarycloud/geometry.py +107 -9
- luminarycloud/geometry_version.py +57 -3
- luminarycloud/mesh.py +1 -2
- luminarycloud/meshing/mesh_generation_params.py +8 -8
- luminarycloud/params/enum/_enum_wrappers.py +562 -30
- luminarycloud/params/simulation/adaptive_mesh_refinement_.py +4 -0
- luminarycloud/params/simulation/material/material_solid_.py +15 -1
- luminarycloud/params/simulation/physics/__init__.py +0 -1
- luminarycloud/params/simulation/physics/periodic_pair_.py +12 -31
- luminarycloud/physics_ai/architectures.py +58 -0
- luminarycloud/physics_ai/inference.py +13 -13
- luminarycloud/physics_ai/solution.py +3 -1
- luminarycloud/physics_ai/training_jobs.py +37 -0
- luminarycloud/pipelines/__init__.py +11 -3
- luminarycloud/pipelines/api.py +248 -16
- luminarycloud/pipelines/arguments.py +15 -0
- luminarycloud/pipelines/core.py +113 -96
- luminarycloud/pipelines/{operators.py → stages.py} +96 -39
- luminarycloud/project.py +15 -47
- luminarycloud/simulation.py +69 -5
- luminarycloud/simulation_param.py +0 -9
- luminarycloud/simulation_template.py +2 -1
- luminarycloud/types/matrix3.py +12 -0
- luminarycloud/vis/__init__.py +17 -0
- luminarycloud/vis/data_extraction.py +20 -4
- luminarycloud/vis/interactive_report.py +110 -0
- luminarycloud/vis/interactive_scene.py +29 -2
- luminarycloud/vis/report.py +252 -0
- luminarycloud/vis/visualization.py +127 -5
- luminarycloud/volume_selection.py +132 -69
- {luminarycloud-0.19.0.dist-info → luminarycloud-0.22.0.dist-info}/METADATA +1 -1
- {luminarycloud-0.19.0.dist-info → luminarycloud-0.22.0.dist-info}/RECORD +105 -97
- luminarycloud/params/simulation/physics/periodic_pair/__init__.py +0 -2
- luminarycloud/params/simulation/physics/periodic_pair/periodicity_type/__init__.py +0 -2
- luminarycloud/params/simulation/physics/periodic_pair/periodicity_type/rotational_periodicity_.py +0 -31
- luminarycloud/params/simulation/physics/periodic_pair/periodicity_type/translational_periodicity_.py +0 -29
- luminarycloud/params/simulation/physics/periodic_pair/periodicity_type_.py +0 -25
- {luminarycloud-0.19.0.dist-info → luminarycloud-0.22.0.dist-info}/WHEEL +0 -0
luminarycloud/project.py
CHANGED
|
@@ -12,6 +12,7 @@ import concurrent
|
|
|
12
12
|
|
|
13
13
|
import luminarycloud as lc
|
|
14
14
|
from luminarycloud._helpers.named_variables import _named_variables_to_proto
|
|
15
|
+
from luminarycloud._helpers.pagination import PaginationIterator
|
|
15
16
|
from luminarycloud.params.simulation.adjoint_ import Adjoint
|
|
16
17
|
|
|
17
18
|
from ._client import get_default_client
|
|
@@ -198,8 +199,6 @@ class Project(ProtoWrapperBase):
|
|
|
198
199
|
def load_geometry_to_setup(self, geometry: "Geometry") -> None:
|
|
199
200
|
"""
|
|
200
201
|
Load a geometry to the setup phase.
|
|
201
|
-
NOTE: this operation is irreversible and deletes all the existing meshes and simulations
|
|
202
|
-
in the project.
|
|
203
202
|
|
|
204
203
|
Parameters
|
|
205
204
|
----------
|
|
@@ -324,6 +323,8 @@ class Project(ProtoWrapperBase):
|
|
|
324
323
|
names_to_file_paths: Dict[str, Union[PathLike[Any], str]],
|
|
325
324
|
params: hexmeshpb.HexMeshSpec,
|
|
326
325
|
use_internal_wrap: bool = False,
|
|
326
|
+
request_id: Optional[str] = None,
|
|
327
|
+
n_vcpus: Optional[int] = None,
|
|
327
328
|
) -> "Mesh":
|
|
328
329
|
"""
|
|
329
330
|
Creates a hex mesh. Only for internal use.
|
|
@@ -361,7 +362,14 @@ class Project(ProtoWrapperBase):
|
|
|
361
362
|
|
|
362
363
|
params.use_wrap = use_internal_wrap
|
|
363
364
|
|
|
364
|
-
|
|
365
|
+
if request_id is None:
|
|
366
|
+
request_id = str(uuid.uuid4())
|
|
367
|
+
req = meshpb.CreateHexMeshRequest(
|
|
368
|
+
project_id=self.id,
|
|
369
|
+
hex_mesh_config=params,
|
|
370
|
+
request_id=request_id,
|
|
371
|
+
n_vcpus=n_vcpus,
|
|
372
|
+
)
|
|
365
373
|
|
|
366
374
|
res: meshpb.CreateHexMeshResponse = client.CreateHexMesh(req)
|
|
367
375
|
get_mesh_res = client.GetMesh(meshpb.GetMeshRequest(id=res.mesh_id))
|
|
@@ -785,53 +793,13 @@ def list_projects() -> list[Project]:
|
|
|
785
793
|
return list(iterate_projects())
|
|
786
794
|
|
|
787
795
|
|
|
788
|
-
class ProjectIterator:
|
|
796
|
+
class ProjectIterator(PaginationIterator[Project]):
|
|
789
797
|
"""Iterator class for projects that provides length hint."""
|
|
790
798
|
|
|
791
|
-
def
|
|
792
|
-
|
|
793
|
-
self._page_token: str = ""
|
|
794
|
-
self._total_count: Optional[int] = None
|
|
795
|
-
self._current_page: Optional[list[projectpb.Project]] = None
|
|
796
|
-
self._client = get_default_client()
|
|
797
|
-
self._iterated_count: int = 0
|
|
798
|
-
|
|
799
|
-
def __iter__(self) -> "ProjectIterator":
|
|
800
|
-
return self
|
|
801
|
-
|
|
802
|
-
def __next__(self) -> Project:
|
|
803
|
-
if self._current_page is None:
|
|
804
|
-
self._fetch_next_page()
|
|
805
|
-
|
|
806
|
-
# _current_page really can't be None here, but this assertion is needed to appease mypy
|
|
807
|
-
assert self._current_page is not None
|
|
808
|
-
|
|
809
|
-
if len(self._current_page) == 0:
|
|
810
|
-
if not self._page_token:
|
|
811
|
-
raise StopIteration
|
|
812
|
-
self._fetch_next_page()
|
|
813
|
-
|
|
814
|
-
self._iterated_count += 1
|
|
815
|
-
|
|
816
|
-
return Project(self._current_page.pop(0))
|
|
817
|
-
|
|
818
|
-
def _fetch_next_page(self) -> None:
|
|
819
|
-
req = projectpb.ListProjectsRequest(page_size=self._page_size, page_token=self._page_token)
|
|
799
|
+
def _fetch_page(self, page_size: int, page_token: str) -> tuple[list[Project], str, int]:
|
|
800
|
+
req = projectpb.ListProjectsRequest(page_size=page_size, page_token=page_token)
|
|
820
801
|
res = self._client.ListProjects(req)
|
|
821
|
-
|
|
822
|
-
self._current_page = list(res.projects)
|
|
823
|
-
self._page_token = res.next_page_token
|
|
824
|
-
|
|
825
|
-
# Set length hint on first fetch if available
|
|
826
|
-
if self._total_count is None:
|
|
827
|
-
self._total_count = res.total_count or 0
|
|
828
|
-
|
|
829
|
-
def __length_hint__(self) -> int:
|
|
830
|
-
if self._total_count is None:
|
|
831
|
-
# Fetch first page to get total size if not already fetched
|
|
832
|
-
if self._current_page is None:
|
|
833
|
-
self._fetch_next_page()
|
|
834
|
-
return max(0, (self._total_count or 0) - self._iterated_count)
|
|
802
|
+
return [Project(p) for p in res.projects], res.next_page_token, res.total_count
|
|
835
803
|
|
|
836
804
|
|
|
837
805
|
def iterate_projects(page_size: int = 50) -> ProjectIterator:
|
luminarycloud/simulation.py
CHANGED
|
@@ -57,6 +57,8 @@ class Simulation(ProtoWrapperBase):
|
|
|
57
57
|
"ID of the simulation mesh."
|
|
58
58
|
project_id: ProjectID
|
|
59
59
|
"ID of the project containing this simulation."
|
|
60
|
+
doe_name: str
|
|
61
|
+
"Name of the design of experiments that created this simulation."
|
|
60
62
|
|
|
61
63
|
_proto: simulationpb.Simulation
|
|
62
64
|
|
|
@@ -122,8 +124,7 @@ class Simulation(ProtoWrapperBase):
|
|
|
122
124
|
interval_seconds=interval_seconds,
|
|
123
125
|
timeout_seconds=timeout_seconds,
|
|
124
126
|
)
|
|
125
|
-
self.
|
|
126
|
-
return self.status
|
|
127
|
+
return self.refresh().status
|
|
127
128
|
|
|
128
129
|
def refresh(self) -> "Simulation":
|
|
129
130
|
"""
|
|
@@ -194,6 +195,12 @@ class Simulation(ProtoWrapperBase):
|
|
|
194
195
|
"""
|
|
195
196
|
Downloads surface outputs (e.g. lift, drag, ...) in csv format.
|
|
196
197
|
|
|
198
|
+
Unless `reference_values` is explicitly passed, the Simulation's reference values -- i.e.
|
|
199
|
+
the ones specified when the Simulation was created -- will be used for computing
|
|
200
|
+
non-dimensional output quantities. While the Luminary Cloud UI lets you update the reference
|
|
201
|
+
values on a Simulation result after it has run, those updates only affect the output
|
|
202
|
+
calculations seen in the UI, they have no effect on the ones retrieved by this method.
|
|
203
|
+
|
|
197
204
|
Parameters
|
|
198
205
|
----------
|
|
199
206
|
quantity_type : luminarycloud.enum.QuantityType
|
|
@@ -205,9 +212,9 @@ class Simulation(ProtoWrapperBase):
|
|
|
205
212
|
Other Parameters
|
|
206
213
|
----------------
|
|
207
214
|
reference_values : ReferenceValues, optional
|
|
208
|
-
Reference values used for computing forces, moments and
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
Reference values used for computing forces, moments, and other non-dimensional output
|
|
216
|
+
quantities. If not provided, the simulation's reference values will be used, i.e., the
|
|
217
|
+
ones specified when the simulation was created.
|
|
211
218
|
calculation_type : CalculationType, optional
|
|
212
219
|
Whether the calculation should be done for all the surfaces together or each surface
|
|
213
220
|
individually. Default is AGGREGATE.
|
|
@@ -304,6 +311,19 @@ class Simulation(ProtoWrapperBase):
|
|
|
304
311
|
req = simulationpb.GetSimulationParametersRequest(id=self.id)
|
|
305
312
|
return SimulationParam.from_proto(get_default_client().GetSimulationParameters(req))
|
|
306
313
|
|
|
314
|
+
def _get_workflow_id(self) -> Optional[str]:
|
|
315
|
+
"""
|
|
316
|
+
Retrieves the workflow ID associated with the current simulation.
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
str | None
|
|
321
|
+
The workflow ID corresponding to this simulation's ID, or None if the simulation
|
|
322
|
+
has no associated workflow.
|
|
323
|
+
"""
|
|
324
|
+
result = _get_workflow_ids([self.id])
|
|
325
|
+
return result.get(self.id)
|
|
326
|
+
|
|
307
327
|
@deprecated(
|
|
308
328
|
"Use get_parameters() instead. This method will be removed in a future release.",
|
|
309
329
|
)
|
|
@@ -354,3 +374,47 @@ def get_simulation(id: SimulationID) -> Simulation:
|
|
|
354
374
|
req = simulationpb.GetSimulationRequest(id=id)
|
|
355
375
|
res = get_default_client().GetSimulation(req)
|
|
356
376
|
return Simulation(res.simulation)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _get_workflow_ids(simulation_ids: list[SimulationID]) -> dict[SimulationID, str]:
|
|
380
|
+
"""
|
|
381
|
+
Get the workflow IDs corresponding to simulation IDs.
|
|
382
|
+
|
|
383
|
+
This is useful for mapping between UI-created simulations (which have workflow IDs)
|
|
384
|
+
and the simulation IDs used in the API.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
simulation_ids : list[SimulationID]
|
|
389
|
+
The simulation IDs to look up.
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
dict[SimulationID, str]
|
|
394
|
+
A mapping from simulation ID to workflow ID. Only simulation IDs that were
|
|
395
|
+
successfully resolved to workflow IDs are present as keys. Simulation IDs are
|
|
396
|
+
omitted if:
|
|
397
|
+
|
|
398
|
+
- The simulation ID does not exist
|
|
399
|
+
- The user lacks access to the simulation's project
|
|
400
|
+
- The simulation has no associated workflow ID
|
|
401
|
+
|
|
402
|
+
Examples
|
|
403
|
+
--------
|
|
404
|
+
>>> import luminarycloud as lc
|
|
405
|
+
>>> sim_ids = [lc.SimulationID("sim_123"), lc.SimulationID("sim_456")]
|
|
406
|
+
>>> workflow_ids = lc._get_workflow_ids(sim_ids)
|
|
407
|
+
>>> print(workflow_ids)
|
|
408
|
+
{SimulationID('sim_123'): 'wf_abc', SimulationID('sim_456'): 'wf_def'}
|
|
409
|
+
|
|
410
|
+
>>> # Check if a simulation has a workflow
|
|
411
|
+
>>> sim_id = lc.SimulationID("sim_123")
|
|
412
|
+
>>> if sim_id in workflow_ids:
|
|
413
|
+
... print(f"Workflow ID: {workflow_ids[sim_id]}")
|
|
414
|
+
... else:
|
|
415
|
+
... print("No workflow found")
|
|
416
|
+
"""
|
|
417
|
+
req = simulationpb.GetWorkflowIDsRequest(simulation_ids=simulation_ids)
|
|
418
|
+
res = get_default_client().GetWorkflowIDs(req)
|
|
419
|
+
# Return dict with SimulationID keys (not str keys)
|
|
420
|
+
return {SimulationID(sim_id): wf_id for sim_id, wf_id in res.data.items()}
|
|
@@ -232,15 +232,6 @@ class SimulationParam(_SimulationParam):
|
|
|
232
232
|
f"physics {_stringify_identifier(v.physics_identifier)}. Overwriting..."
|
|
233
233
|
),
|
|
234
234
|
)
|
|
235
|
-
_remove_from_list_with_warning(
|
|
236
|
-
_list=volume_physics_pairs,
|
|
237
|
-
_accessor=lambda v: get_id(v.physics_identifier),
|
|
238
|
-
_to_remove=get_id(physics.physics_identifier),
|
|
239
|
-
_warning_message=lambda v: (
|
|
240
|
-
f"Physics {_stringify_identifier(physics.physics_identifier)} has already been "
|
|
241
|
-
f"assigned to volume {_stringify_identifier(v.volume_identifier)}. Overwriting..."
|
|
242
|
-
),
|
|
243
|
-
)
|
|
244
235
|
|
|
245
236
|
if volume_identifier.id not in (get_id(v.volume_identifier) for v in self.volume_entity):
|
|
246
237
|
self.volume_entity.append(VolumeEntity(volume_identifier=volume_identifier))
|
|
@@ -490,7 +490,8 @@ class SimulationTemplate(ProtoWrapperBase):
|
|
|
490
490
|
code += "\n\n\n"
|
|
491
491
|
code += '# Create a new simulation template or modify the one that is synced with the UI "Setup" tab.\n'
|
|
492
492
|
code += f'project = luminarycloud.get_project("{self.project_id}")\n'
|
|
493
|
-
|
|
493
|
+
escaped_name = self.name.replace('\\','\\\\').replace('"','\\"')
|
|
494
|
+
code += f'template = project.create_simulation_template(name="{escaped_name}")\n'
|
|
494
495
|
code += '# TODO(USER): To modify the "Setup" template, uncomment the line below and comment out the line above.\n'
|
|
495
496
|
code += "# template = project.list_simulation_templates()[0] # Setup template\n\n"
|
|
496
497
|
|
luminarycloud/types/matrix3.py
CHANGED
|
@@ -20,7 +20,19 @@ class Matrix3:
|
|
|
20
20
|
c=basepb.Vector3(x=self.c.x, y=self.c.y, z=self.c.z),
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
def _to_ad_proto(self) -> basepb.AdMatrix3:
|
|
24
|
+
return basepb.AdMatrix3(
|
|
25
|
+
a=self.a._to_ad_proto(),
|
|
26
|
+
b=self.b._to_ad_proto(),
|
|
27
|
+
c=self.c._to_ad_proto(),
|
|
28
|
+
)
|
|
29
|
+
|
|
23
30
|
def _from_proto(self, proto: basepb.Matrix3) -> None:
|
|
24
31
|
self.a = Vector3(x=proto.a.x, y=proto.a.y, z=proto.a.z)
|
|
25
32
|
self.b = Vector3(x=proto.b.x, y=proto.b.y, z=proto.b.z)
|
|
26
33
|
self.c = Vector3(x=proto.c.x, y=proto.c.y, z=proto.c.z)
|
|
34
|
+
|
|
35
|
+
def _from_ad_proto(self, proto: basepb.AdMatrix3) -> None:
|
|
36
|
+
self.a = Vector3.from_ad_proto(proto.a)
|
|
37
|
+
self.b = Vector3.from_ad_proto(proto.b)
|
|
38
|
+
self.c = Vector3.from_ad_proto(proto.c)
|
luminarycloud/vis/__init__.py
CHANGED
|
@@ -4,8 +4,14 @@ from .visualization import (
|
|
|
4
4
|
EntityType as EntityType,
|
|
5
5
|
list_renders as list_renders,
|
|
6
6
|
list_quantities as list_quantities,
|
|
7
|
+
list_quantities as list_quantities,
|
|
8
|
+
list_cameras as list_cameras,
|
|
9
|
+
RangeResult as RangeResult,
|
|
10
|
+
range_query as range_query,
|
|
11
|
+
get_camera as get_camera,
|
|
7
12
|
DirectionalCamera as DirectionalCamera,
|
|
8
13
|
LookAtCamera as LookAtCamera,
|
|
14
|
+
CameraEntry as CameraEntry,
|
|
9
15
|
)
|
|
10
16
|
|
|
11
17
|
from .primitives import (
|
|
@@ -52,3 +58,14 @@ from .interactive_scene import (
|
|
|
52
58
|
from .interactive_inference import (
|
|
53
59
|
InteractiveInference as InteractiveInference,
|
|
54
60
|
)
|
|
61
|
+
|
|
62
|
+
# Unreleased/internal for testing now
|
|
63
|
+
|
|
64
|
+
# from .report import (
|
|
65
|
+
# Report as Report,
|
|
66
|
+
# )
|
|
67
|
+
|
|
68
|
+
# from .interactive_report import (
|
|
69
|
+
# InteractiveReport as InteractiveReport,
|
|
70
|
+
# ReportEntry as ReportEntry,
|
|
71
|
+
# )
|
|
@@ -22,6 +22,7 @@ from luminarycloud.params.simulation.physics.fluid.boundary_conditions import Fa
|
|
|
22
22
|
from .._helpers._code_representation import CodeRepr
|
|
23
23
|
from ..types import SimulationID
|
|
24
24
|
from collections import defaultdict
|
|
25
|
+
import copy
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
@@ -227,14 +228,14 @@ class ExtractOutput:
|
|
|
227
228
|
extract_id: str,
|
|
228
229
|
project_id: str,
|
|
229
230
|
name: str,
|
|
230
|
-
|
|
231
|
+
description: str,
|
|
231
232
|
status: ExtractStatusType,
|
|
232
233
|
) -> None:
|
|
233
234
|
self._extract_id = extract_id
|
|
234
235
|
self._project_id = project_id
|
|
235
236
|
self.status = status
|
|
236
237
|
self.name = name
|
|
237
|
-
self.description =
|
|
238
|
+
self.description = description
|
|
238
239
|
|
|
239
240
|
def __repr__(self) -> str:
|
|
240
241
|
return f"ExtractOutput (Id: {self._extract_id} status: {self.status})"
|
|
@@ -563,7 +564,7 @@ class DataExtractor:
|
|
|
563
564
|
extract_id=res.extract.extract_id,
|
|
564
565
|
project_id=self._project_id,
|
|
565
566
|
name=name,
|
|
566
|
-
|
|
567
|
+
description=description,
|
|
567
568
|
status=ExtractStatusType(res.extract.status),
|
|
568
569
|
)
|
|
569
570
|
return extract_output
|
|
@@ -657,6 +658,21 @@ class DataExtractor:
|
|
|
657
658
|
|
|
658
659
|
return imports + code
|
|
659
660
|
|
|
661
|
+
def clone(self, solution: Solution) -> "DataExtractor":
|
|
662
|
+
"""
|
|
663
|
+
Clone this extract based on a new solution. This is a deep copy
|
|
664
|
+
operation. Both solution must be compatible with one another, meaning
|
|
665
|
+
they share tags or surfaces ids for extractors such as
|
|
666
|
+
IntersectionCurve.
|
|
667
|
+
"""
|
|
668
|
+
if not isinstance(solution, Solution):
|
|
669
|
+
raise TypeError("Expected a Solution object.")
|
|
670
|
+
cloned = DataExtractor(solution)
|
|
671
|
+
for extract in self._extracts:
|
|
672
|
+
copied_extract = copy.deepcopy(extract)
|
|
673
|
+
cloned.add_data_extract(copied_extract)
|
|
674
|
+
return cloned
|
|
675
|
+
|
|
660
676
|
|
|
661
677
|
def list_data_extracts(solution: Solution) -> list[ExtractOutput]:
|
|
662
678
|
"""
|
|
@@ -700,7 +716,7 @@ def list_data_extracts(solution: Solution) -> list[ExtractOutput]:
|
|
|
700
716
|
extract_id=extract.extract_id,
|
|
701
717
|
project_id=extract.project_id,
|
|
702
718
|
name=extract.name,
|
|
703
|
-
|
|
719
|
+
description=extract.description,
|
|
704
720
|
status=ExtractStatusType(extract.status),
|
|
705
721
|
)
|
|
706
722
|
# This need to be fixed on the backend, but manually refreshing works for now.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from .visualization import RenderOutput
|
|
3
|
+
from .report import ReportEntry
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import luminarycloud_jupyter as lcj
|
|
7
|
+
except ImportError:
|
|
8
|
+
lcj = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InteractiveReport:
|
|
12
|
+
"""
|
|
13
|
+
Interactive report widget with lazy loading for large datasets.
|
|
14
|
+
|
|
15
|
+
How it works:
|
|
16
|
+
1. on initialization:
|
|
17
|
+
- sends metadata for all rows (for filtering/selection)
|
|
18
|
+
- downloads the first row (to determine grid dimensions)
|
|
19
|
+
- other rows remain unloaded
|
|
20
|
+
|
|
21
|
+
2. on user request:
|
|
22
|
+
- _load_row_data() is called with row index
|
|
23
|
+
- downloads and sends images/plots for that specific row
|
|
24
|
+
- python sets row_states to 'loading' -> 'loaded' (or 'error')
|
|
25
|
+
|
|
26
|
+
This allows working with 1000+ row datasets without waiting for all data upfront.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# TODO Will/Matt: this list of report entries could be how we store stuff in the DB
|
|
30
|
+
# for interactive reports, to reference the post proc. extracts. A report is essentially
|
|
31
|
+
# a bunch of extracts + metadata.
|
|
32
|
+
def __init__(self, entries: list[ReportEntry]) -> None:
|
|
33
|
+
if not lcj:
|
|
34
|
+
raise ImportError("InteractiveScene requires luminarycloud[jupyter] to be installed")
|
|
35
|
+
|
|
36
|
+
self.entries = entries
|
|
37
|
+
if len(self.entries) == 0:
|
|
38
|
+
raise ValueError("Invalid number of entries, must be > 0")
|
|
39
|
+
|
|
40
|
+
# Determine grid dimensions by downloading first entry
|
|
41
|
+
# to understand the structure (number of columns)
|
|
42
|
+
first_entry = self.entries[0]
|
|
43
|
+
first_entry.download_extracts()
|
|
44
|
+
|
|
45
|
+
# Calculate actual number of columns by counting how many cells
|
|
46
|
+
# each extract produces (RenderOutput can produce multiple images)
|
|
47
|
+
ncols = 0
|
|
48
|
+
for extract in first_entry._extracts:
|
|
49
|
+
if isinstance(extract, RenderOutput):
|
|
50
|
+
image_and_label = extract.download_images()
|
|
51
|
+
ncols += len(image_and_label)
|
|
52
|
+
else:
|
|
53
|
+
ncols += 1 # Plot data extracts produce one cell
|
|
54
|
+
|
|
55
|
+
nrows = len(self.entries)
|
|
56
|
+
|
|
57
|
+
# Create widget with metadata but without data
|
|
58
|
+
self.widget = lcj.EnsembleWidget([re._metadata for re in self.entries], nrows, ncols)
|
|
59
|
+
|
|
60
|
+
# Set the callback for lazy loading row data
|
|
61
|
+
self.widget.set_row_data_callback(self._load_row_data)
|
|
62
|
+
|
|
63
|
+
def _load_row_data(self, row: int) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Load and send data for a specific row to the widget.
|
|
66
|
+
This is called on-demand when the user requests data for a row.
|
|
67
|
+
"""
|
|
68
|
+
re = self.entries[row]
|
|
69
|
+
|
|
70
|
+
# Download extracts if not already downloaded
|
|
71
|
+
if len(re._extracts) == 0:
|
|
72
|
+
re.download_extracts()
|
|
73
|
+
|
|
74
|
+
# Process each extract and send to widget
|
|
75
|
+
# Track the actual column index as we may have multiple cells per extract
|
|
76
|
+
col = 0
|
|
77
|
+
for extract in re._extracts:
|
|
78
|
+
if isinstance(extract, RenderOutput):
|
|
79
|
+
image_and_label = extract.download_images()
|
|
80
|
+
# Each image gets its own column
|
|
81
|
+
for il in image_and_label:
|
|
82
|
+
self.widget.set_cell_data(row, col, il[0].getvalue(), "jpg")
|
|
83
|
+
col += 1
|
|
84
|
+
else:
|
|
85
|
+
plot_data = extract.download_data()
|
|
86
|
+
data = plot_data[0][0]
|
|
87
|
+
all_axis_labels = data[0]
|
|
88
|
+
|
|
89
|
+
axis_data = []
|
|
90
|
+
for axis_idx in range(len(all_axis_labels)):
|
|
91
|
+
axis_values = [row[axis_idx] for row in data[1:]]
|
|
92
|
+
axis_data.append(axis_values)
|
|
93
|
+
|
|
94
|
+
self.widget.set_cell_scatter_plot(
|
|
95
|
+
row,
|
|
96
|
+
col,
|
|
97
|
+
f"Row #{row} - Multi-axis Plot",
|
|
98
|
+
all_axis_labels,
|
|
99
|
+
axis_data,
|
|
100
|
+
plot_name=f"plot-{row}",
|
|
101
|
+
plot_mode="markers",
|
|
102
|
+
)
|
|
103
|
+
col += 1
|
|
104
|
+
|
|
105
|
+
def _ipython_display_(self) -> None:
|
|
106
|
+
"""
|
|
107
|
+
When the InteractiveReport is shown in Jupyter we show the underlying widget
|
|
108
|
+
to run the widget's frontend code
|
|
109
|
+
"""
|
|
110
|
+
self.widget._ipython_display_()
|
|
@@ -18,7 +18,16 @@ try:
|
|
|
18
18
|
import luminarycloud_jupyter as lcj
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
-
from luminarycloud_jupyter import
|
|
21
|
+
from luminarycloud_jupyter import (
|
|
22
|
+
InteractiveLCVisWidget,
|
|
23
|
+
LCVisPlaneWidget,
|
|
24
|
+
LCVisLineWidget,
|
|
25
|
+
LCVisBoxWidget,
|
|
26
|
+
LCVisCylinderWidget,
|
|
27
|
+
LCVisHalfSphereWidget,
|
|
28
|
+
LCVisSphereWidget,
|
|
29
|
+
LCVisFinitePlaneWidget,
|
|
30
|
+
)
|
|
22
31
|
except ImportError:
|
|
23
32
|
lcj = None
|
|
24
33
|
|
|
@@ -134,7 +143,7 @@ class InteractiveScene:
|
|
|
134
143
|
def set_surface_visibility(self, surface_id: str, visible: bool) -> None:
|
|
135
144
|
self.widget.set_surface_visibility(surface_id, visible)
|
|
136
145
|
|
|
137
|
-
def set_surface_color(self, surface_id: str, color: list[float]) -> None:
|
|
146
|
+
def set_surface_color(self, surface_id: str, color: "list[float]") -> None:
|
|
138
147
|
self.widget.set_surface_color(surface_id, color)
|
|
139
148
|
|
|
140
149
|
def set_display_attributes(self, object_id: str, attrs: DisplayAttributes) -> None:
|
|
@@ -199,6 +208,24 @@ class InteractiveScene:
|
|
|
199
208
|
def add_plane_widget(self) -> "LCVisPlaneWidget":
|
|
200
209
|
return self.widget.add_plane_widget()
|
|
201
210
|
|
|
211
|
+
def add_line_widget(self) -> "LCVisLineWidget":
|
|
212
|
+
return self.widget.add_line_widget()
|
|
213
|
+
|
|
214
|
+
def add_box_widget(self) -> "LCVisBoxWidget":
|
|
215
|
+
return self.widget.add_box_widget()
|
|
216
|
+
|
|
217
|
+
def add_cylinder_widget(self) -> "LCVisCylinderWidget":
|
|
218
|
+
return self.widget.add_cylinder_widget()
|
|
219
|
+
|
|
220
|
+
def add_half_sphere_widget(self) -> "LCVisHalfSphereWidget":
|
|
221
|
+
return self.widget.add_half_sphere_widget()
|
|
222
|
+
|
|
223
|
+
def add_finite_plane_widget(self) -> "LCVisFinitePlaneWidget":
|
|
224
|
+
return self.widget.add_finite_plane_widget()
|
|
225
|
+
|
|
226
|
+
def add_sphere_widget(self) -> "LCVisSphereWidget":
|
|
227
|
+
return self.widget.add_sphere_widget()
|
|
228
|
+
|
|
202
229
|
def delete_widget(self, widget: "InteractiveLCVisWidget") -> None:
|
|
203
230
|
self.widget.delete_widget(widget)
|
|
204
231
|
|