luminarycloud 0.16.1__py3-none-any.whl → 0.17.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/_auth/auth.py +23 -34
- luminarycloud/_client/client.py +21 -5
- luminarycloud/_client/retry_interceptor.py +7 -0
- luminarycloud/_helpers/_create_geometry.py +0 -2
- luminarycloud/_helpers/_wait_for_mesh.py +14 -4
- luminarycloud/_helpers/warnings/__init__.py +0 -1
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +120 -120
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +2 -8
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.py +25 -3
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.pyi +30 -0
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2.py +97 -0
- luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2.pyi +93 -0
- luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2_grpc.py +132 -0
- luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2_grpc.pyi +44 -0
- luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.py +88 -34
- luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.pyi +96 -6
- luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.py +68 -0
- luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.pyi +24 -0
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.py +153 -133
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.pyi +51 -3
- luminarycloud/_proto/cad/shape_pb2.py +78 -19
- luminarycloud/_proto/cad/transformation_pb2.py +34 -15
- luminarycloud/_proto/geometry/geometry_pb2.py +62 -62
- luminarycloud/_proto/geometry/geometry_pb2.pyi +3 -5
- luminarycloud/_proto/hexmesh/hexmesh_pb2.py +17 -4
- luminarycloud/_proto/hexmesh/hexmesh_pb2.pyi +22 -1
- luminarycloud/_proto/quantity/quantity_pb2.py +19 -19
- luminarycloud/_proto/upload/upload_pb2.py +25 -15
- luminarycloud/_proto/upload/upload_pb2.pyi +31 -2
- luminarycloud/feature_modification.py +0 -5
- luminarycloud/geometry.py +15 -2
- luminarycloud/mesh.py +16 -0
- luminarycloud/named_variable_set.py +3 -4
- luminarycloud/outputs/stopping_conditions.py +0 -3
- luminarycloud/physics_ai/architectures.py +0 -4
- luminarycloud/physics_ai/inference.py +0 -4
- luminarycloud/physics_ai/models.py +0 -4
- luminarycloud/physics_ai/solution.py +2 -2
- luminarycloud/pipelines/__init__.py +6 -0
- luminarycloud/pipelines/arguments.py +105 -0
- luminarycloud/pipelines/core.py +204 -20
- luminarycloud/pipelines/operators.py +11 -9
- luminarycloud/pipelines/parameters.py +25 -4
- luminarycloud/project.py +37 -11
- luminarycloud/simulation_param.py +0 -2
- luminarycloud/simulation_template.py +1 -3
- luminarycloud/solution.py +1 -3
- luminarycloud/vis/__init__.py +2 -0
- luminarycloud/vis/data_extraction.py +201 -31
- luminarycloud/vis/filters.py +94 -35
- luminarycloud/vis/primitives.py +78 -1
- luminarycloud/vis/visualization.py +44 -6
- luminarycloud/volume_selection.py +0 -4
- {luminarycloud-0.16.1.dist-info → luminarycloud-0.17.0.dist-info}/METADATA +1 -1
- {luminarycloud-0.16.1.dist-info → luminarycloud-0.17.0.dist-info}/RECORD +58 -54
- luminarycloud/_helpers/warnings/experimental.py +0 -48
- {luminarycloud-0.16.1.dist-info → luminarycloud-0.17.0.dist-info}/WHEEL +0 -0
|
@@ -4,7 +4,7 @@ import csv
|
|
|
4
4
|
import json
|
|
5
5
|
from .vis_util import _download_file, _InternalToken, generate_id, _get_status
|
|
6
6
|
from ..enum import ExtractStatusType, EntityType
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Tuple, cast, Union
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from .._proto.api.v0.luminarycloud.vis import vis_pb2
|
|
10
10
|
from .primitives import Plane
|
|
@@ -12,19 +12,22 @@ from ..types.vector3 import _to_vector3, Vector3Like, Vector3
|
|
|
12
12
|
from .._client import get_default_client
|
|
13
13
|
import logging
|
|
14
14
|
from ..solution import Solution
|
|
15
|
-
from ..geometry import Geometry
|
|
15
|
+
from ..geometry import Geometry
|
|
16
16
|
from ..mesh import Mesh, get_mesh, get_mesh_metadata
|
|
17
17
|
from ..simulation import get_simulation
|
|
18
18
|
from .._helpers._get_project_id import _get_project_id
|
|
19
19
|
from .display import DisplayAttributes
|
|
20
20
|
from time import sleep, time
|
|
21
21
|
from luminarycloud.params.simulation.physics.fluid.boundary_conditions import Farfield
|
|
22
|
-
from ..
|
|
22
|
+
from .._helpers._code_representation import CodeRepr
|
|
23
|
+
from ..types import SimulationID
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
logger = logging.getLogger(__name__)
|
|
25
28
|
|
|
26
29
|
|
|
27
|
-
class DataExtract(ABC):
|
|
30
|
+
class DataExtract(ABC, CodeRepr):
|
|
28
31
|
"""
|
|
29
32
|
This is the base class for all data extracts. Each derived extract class
|
|
30
33
|
is responsible for providing a _to_proto method to convert to a filter
|
|
@@ -65,10 +68,10 @@ class IntersectionCurve(DataExtract):
|
|
|
65
68
|
A user provided name for the filter.
|
|
66
69
|
"""
|
|
67
70
|
|
|
68
|
-
def __init__(self, name: str) -> None:
|
|
71
|
+
def __init__(self, name: str = "") -> None:
|
|
69
72
|
super().__init__(generate_id("intersection-curve"))
|
|
70
73
|
self.name = name
|
|
71
|
-
self._surface_names:
|
|
74
|
+
self._surface_names: list[str] = []
|
|
72
75
|
self._plane = Plane()
|
|
73
76
|
self.label: str = ""
|
|
74
77
|
|
|
@@ -98,7 +101,7 @@ class IntersectionCurve(DataExtract):
|
|
|
98
101
|
raise TypeError(f"Expected 'str', got {type(id).__name__}")
|
|
99
102
|
self._surface_names.append(id)
|
|
100
103
|
|
|
101
|
-
def _surfaces(self) ->
|
|
104
|
+
def _surfaces(self) -> list[str]:
|
|
102
105
|
"""
|
|
103
106
|
Returns the current list of surfaces.
|
|
104
107
|
"""
|
|
@@ -113,14 +116,25 @@ class IntersectionCurve(DataExtract):
|
|
|
113
116
|
for id in self._surface_names:
|
|
114
117
|
vis_filter.intersection_curve.surfaces.append(id)
|
|
115
118
|
|
|
116
|
-
vis_filter.intersection_curve.plane.
|
|
117
|
-
_to_vector3(self.plane.origin)._to_proto()
|
|
118
|
-
)
|
|
119
|
-
vis_filter.intersection_curve.plane.normal.CopyFrom(
|
|
120
|
-
_to_vector3(self.plane.normal)._to_proto()
|
|
121
|
-
)
|
|
119
|
+
vis_filter.intersection_curve.plane.CopyFrom(self._plane._to_proto())
|
|
122
120
|
return vis_filter
|
|
123
121
|
|
|
122
|
+
def _from_proto(self, vis_filter: vis_pb2.Filter) -> None:
|
|
123
|
+
self.id = vis_filter.id
|
|
124
|
+
self.name = vis_filter.name
|
|
125
|
+
self.label = vis_filter.intersection_curve.label
|
|
126
|
+
self._surface_names = list(vis_filter.intersection_curve.surfaces)
|
|
127
|
+
self._plane = Plane()
|
|
128
|
+
self._plane._from_proto(vis_filter.intersection_curve.plane)
|
|
129
|
+
|
|
130
|
+
def _to_code(self, hide_defaults: bool = True, use_tmp_objs: bool = True) -> str:
|
|
131
|
+
code = super()._to_code(hide_defaults=hide_defaults)
|
|
132
|
+
# We need to explicity write the code for the surfaces since its
|
|
133
|
+
# technically a private variable.
|
|
134
|
+
for s in self._surface_names:
|
|
135
|
+
code += f".add_surface('{s}')\n"
|
|
136
|
+
return code
|
|
137
|
+
|
|
124
138
|
|
|
125
139
|
class LineSample(DataExtract):
|
|
126
140
|
"""
|
|
@@ -144,7 +158,7 @@ class LineSample(DataExtract):
|
|
|
144
158
|
A user provided name for the filter.
|
|
145
159
|
"""
|
|
146
160
|
|
|
147
|
-
def __init__(self, name: str) -> None:
|
|
161
|
+
def __init__(self, name: str = "") -> None:
|
|
148
162
|
super().__init__(generate_id("line-sample"))
|
|
149
163
|
self.name = name
|
|
150
164
|
self.start: Vector3Like = Vector3(x=0, y=0, z=0)
|
|
@@ -160,6 +174,15 @@ class LineSample(DataExtract):
|
|
|
160
174
|
vis_filter.line_sample.end.CopyFrom(_to_vector3(self.end)._to_proto())
|
|
161
175
|
return vis_filter
|
|
162
176
|
|
|
177
|
+
def _from_proto(self, vis_filter: vis_pb2.Filter) -> None:
|
|
178
|
+
self.id = vis_filter.id
|
|
179
|
+
self.name = vis_filter.name
|
|
180
|
+
self.label = vis_filter.line_sample.label
|
|
181
|
+
self.start = Vector3()
|
|
182
|
+
self.start._from_proto(vis_filter.line_sample.start)
|
|
183
|
+
self.end = Vector3()
|
|
184
|
+
self.end._from_proto(vis_filter.line_sample.end)
|
|
185
|
+
|
|
163
186
|
|
|
164
187
|
class ExtractOutput:
|
|
165
188
|
"""
|
|
@@ -261,7 +284,7 @@ class ExtractOutput:
|
|
|
261
284
|
raise TimeoutError
|
|
262
285
|
sleep(max(-1, min(interval_seconds, deadline - time())))
|
|
263
286
|
|
|
264
|
-
def download_data(self) ->
|
|
287
|
+
def download_data(self) -> list[Tuple[list[list[Union[str, int, float]]], str]]:
|
|
265
288
|
"""
|
|
266
289
|
Downloads the resulting data into memory. This is useful
|
|
267
290
|
for plotting data in notebooks. If that status is not complete, an
|
|
@@ -285,7 +308,7 @@ class ExtractOutput:
|
|
|
285
308
|
req.project_id = self._project_id
|
|
286
309
|
res: vis_pb2.DownloadExtractResponse = get_default_client().DownloadExtract(req)
|
|
287
310
|
|
|
288
|
-
csv_files:
|
|
311
|
+
csv_files: list[Tuple[list[list[Union[str, int, float]]], str]] = []
|
|
289
312
|
if res.HasField("line_data"):
|
|
290
313
|
compressed_buffer = _download_file(res.line_data)
|
|
291
314
|
dctx = zstd.ZstdDecompressor()
|
|
@@ -300,7 +323,7 @@ class ExtractOutput:
|
|
|
300
323
|
ids = line_data.lines.keys()
|
|
301
324
|
# Each filter(id) produces a set of tables, one per line segment.
|
|
302
325
|
for id in ids:
|
|
303
|
-
header:
|
|
326
|
+
header: list[Union[str, float, int]] = []
|
|
304
327
|
tables = line_data.lines[id]
|
|
305
328
|
# One table per line segment. First, figure out the
|
|
306
329
|
# shape of the data and validate what we expect.
|
|
@@ -321,15 +344,15 @@ class ExtractOutput:
|
|
|
321
344
|
assert n_rows * n_cols == len(table.record[0].entry)
|
|
322
345
|
assert len(header) != 0
|
|
323
346
|
assert n_cols != 0
|
|
324
|
-
rows:
|
|
347
|
+
rows: list[list[Union[str, float, int]]] = []
|
|
325
348
|
rows.append(header)
|
|
326
349
|
for curve_id, table in enumerate(tables.lines_table):
|
|
327
350
|
n_rows = len(table.axis[0].coordinate)
|
|
328
|
-
new_rows:
|
|
351
|
+
new_rows: list[list[Union[str, float, int]]] = []
|
|
329
352
|
idx = 0
|
|
330
353
|
# The the shape of the values are in row-major ordering.
|
|
331
354
|
for r in range(n_rows):
|
|
332
|
-
row:
|
|
355
|
+
row: list[Union[str, float, int]] = []
|
|
333
356
|
for c in range(n_cols):
|
|
334
357
|
row.append(table.record[0].entry[idx].adfloat.value)
|
|
335
358
|
idx += 1
|
|
@@ -363,7 +386,7 @@ class ExtractOutput:
|
|
|
363
386
|
raise ValueError("file_prefix must be non-empty")
|
|
364
387
|
|
|
365
388
|
csv_files = self.download_data()
|
|
366
|
-
names_labels:
|
|
389
|
+
names_labels: list[Tuple[str, str]] = []
|
|
367
390
|
counter = 0
|
|
368
391
|
for csv_file in csv_files:
|
|
369
392
|
output_file = f"{file_prefix}_{counter}.csv"
|
|
@@ -390,6 +413,20 @@ class ExtractOutput:
|
|
|
390
413
|
self._deleted = True
|
|
391
414
|
|
|
392
415
|
|
|
416
|
+
def _data_extract_to_obj_name(extract: DataExtract) -> str:
|
|
417
|
+
"""
|
|
418
|
+
Helper function to convert a filter to a code object name used in code gen.
|
|
419
|
+
"""
|
|
420
|
+
if not isinstance(extract, DataExtract):
|
|
421
|
+
raise TypeError(f"Expected 'DataExtract', got {type(extract).__name__}")
|
|
422
|
+
if isinstance(extract, LineSample):
|
|
423
|
+
return "line_sample"
|
|
424
|
+
elif isinstance(extract, IntersectionCurve):
|
|
425
|
+
return "intersection_curve"
|
|
426
|
+
else:
|
|
427
|
+
raise TypeError(f"Unknown data extract type: {type(extract).__name__}")
|
|
428
|
+
|
|
429
|
+
|
|
393
430
|
class DataExtractor:
|
|
394
431
|
"""
|
|
395
432
|
I extract data from solutions.
|
|
@@ -403,7 +440,7 @@ class DataExtractor:
|
|
|
403
440
|
raise TypeError(f"Expected Solution got {type(solution).__name__}")
|
|
404
441
|
self._solution: Solution = solution
|
|
405
442
|
self._entity_type: EntityType = EntityType.SIMULATION
|
|
406
|
-
self._extracts:
|
|
443
|
+
self._extracts: list[DataExtract] = []
|
|
407
444
|
|
|
408
445
|
# Meshes that are directly uploaded will not have tags.
|
|
409
446
|
self._has_tags: bool = True
|
|
@@ -426,18 +463,18 @@ class DataExtractor:
|
|
|
426
463
|
else:
|
|
427
464
|
geom = geo_ver.geometry()
|
|
428
465
|
|
|
429
|
-
self._surface_ids:
|
|
466
|
+
self._surface_ids: list[str] = []
|
|
430
467
|
for zone in mesh_meta.zones:
|
|
431
468
|
for bound in zone.boundaries:
|
|
432
469
|
self._surface_ids.append(bound.name)
|
|
433
470
|
|
|
434
|
-
self._tag_ids:
|
|
471
|
+
self._tag_ids: list[str] = []
|
|
435
472
|
if geom and self._has_tags:
|
|
436
473
|
tags = geom.list_tags()
|
|
437
474
|
for tag in tags:
|
|
438
475
|
self._tag_ids.append(tag.id)
|
|
439
476
|
|
|
440
|
-
self.far_field_boundary_ids:
|
|
477
|
+
self.far_field_boundary_ids: list[str] = []
|
|
441
478
|
|
|
442
479
|
# Find all the far field surfaces if we can get the params.
|
|
443
480
|
params = simulation.get_parameters()
|
|
@@ -448,12 +485,12 @@ class DataExtractor:
|
|
|
448
485
|
for bc_surface in bc.surfaces:
|
|
449
486
|
self.far_field_boundary_ids.append(bc_surface)
|
|
450
487
|
|
|
451
|
-
def _validate_surfaces_and_tags(self, ids:
|
|
488
|
+
def _validate_surfaces_and_tags(self, ids: list[str]) -> list[str]:
|
|
452
489
|
"""
|
|
453
490
|
Validate a list of ids as either tags or ids. Returns a list of invalid ids. If the
|
|
454
491
|
length of the list is zero, the input list is valid.
|
|
455
492
|
"""
|
|
456
|
-
bad_ids:
|
|
493
|
+
bad_ids: list[str] = []
|
|
457
494
|
for id in ids:
|
|
458
495
|
if id in self._tag_ids:
|
|
459
496
|
continue
|
|
@@ -461,11 +498,11 @@ class DataExtractor:
|
|
|
461
498
|
bad_ids.append(id)
|
|
462
499
|
return bad_ids
|
|
463
500
|
|
|
464
|
-
def surface_ids(self) ->
|
|
501
|
+
def surface_ids(self) -> list[str]:
|
|
465
502
|
"""Get a list of all the surface ids associated with the solution."""
|
|
466
503
|
return self._surface_ids
|
|
467
504
|
|
|
468
|
-
def tag_ids(self) ->
|
|
505
|
+
def tag_ids(self) -> list[str]:
|
|
469
506
|
"""Get a list of all the tag ids associated with the solution."""
|
|
470
507
|
return self._tag_ids
|
|
471
508
|
|
|
@@ -531,8 +568,97 @@ class DataExtractor:
|
|
|
531
568
|
)
|
|
532
569
|
return extract_output
|
|
533
570
|
|
|
571
|
+
def to_code(self, obj_name: str, include_imports: bool, hide_defaults: bool = True) -> str:
|
|
572
|
+
"""
|
|
573
|
+
This function will produce a code string that reproduces the data extractor
|
|
574
|
+
in its current state.
|
|
534
575
|
|
|
535
|
-
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
obj_name: str
|
|
579
|
+
the object name of the scene.
|
|
580
|
+
include_imports: bool
|
|
581
|
+
If True, the code will include the necessary imports to run the code. This will be
|
|
582
|
+
set to false if generating scene code as well, since the imports overlap.
|
|
583
|
+
hide_defaults: bool, optional
|
|
584
|
+
If True, the code will make a best effort not include default values for attributes.
|
|
585
|
+
"""
|
|
586
|
+
if len(self._extracts) == 0:
|
|
587
|
+
# If we don't have any extracts, we don't need to generate any code.
|
|
588
|
+
return ""
|
|
589
|
+
|
|
590
|
+
imports: str = ""
|
|
591
|
+
if include_imports:
|
|
592
|
+
imports += "import luminarycloud as lc\n"
|
|
593
|
+
imports += "import luminarycloud.vis as vis\n"
|
|
594
|
+
imports += "from luminarycloud.types import Vector3\n"
|
|
595
|
+
imports += "from luminarycloud.enum import ExtractStatusType\n"
|
|
596
|
+
|
|
597
|
+
# This isn't technically needed, but I think its useful.
|
|
598
|
+
code = "\n# Find the entity to build the scene from\n"
|
|
599
|
+
code += f"simulation = lc.get_simulation('{self._solution.simulation_id}')\n"
|
|
600
|
+
code += "for sol in simulation.list_solutions():\n"
|
|
601
|
+
code += f" if sol.id == '{self._solution.id}':\n"
|
|
602
|
+
code += f" solution = sol\n"
|
|
603
|
+
code += f" break\n"
|
|
604
|
+
code += "data_extractor = vis.DataExtractor(solution)\n"
|
|
605
|
+
code += "\n"
|
|
606
|
+
|
|
607
|
+
code += "\n"
|
|
608
|
+
# We can have many of the same type of filter so we need to track how
|
|
609
|
+
# many times we have seen a filter type to create the object name.
|
|
610
|
+
name_map: defaultdict[str, int] = defaultdict(int)
|
|
611
|
+
# Filters can be connected so we need to track what the ids are so we
|
|
612
|
+
# can connected them.
|
|
613
|
+
ids_to_obj_name: dict[str, str] = {}
|
|
614
|
+
for extract in self._extracts:
|
|
615
|
+
# Name objects numerically: slice0, slice1, etc.
|
|
616
|
+
name = _data_extract_to_obj_name(extract)
|
|
617
|
+
obj_name = f"{name}{name_map[obj_name]}"
|
|
618
|
+
name_map[obj_name] += 1
|
|
619
|
+
ids_to_obj_name[extract.id] = obj_name
|
|
620
|
+
code += extract._to_code_helper(obj_name, hide_defaults=hide_defaults)
|
|
621
|
+
code += f"data_extractor.add_data_extract({obj_name})\n"
|
|
622
|
+
code += "\n"
|
|
623
|
+
|
|
624
|
+
if include_imports:
|
|
625
|
+
imports += "\n"
|
|
626
|
+
# The code gen is very verbose, so we can do some string replacements
|
|
627
|
+
# since we are importing the luminarycloud.vis package.
|
|
628
|
+
cleanup_list: list[str] = [
|
|
629
|
+
"luminarycloud.vis.data_extraction",
|
|
630
|
+
]
|
|
631
|
+
for cleanup in cleanup_list:
|
|
632
|
+
code = code.replace(cleanup, "vis")
|
|
633
|
+
# Many classes initialize the attributes, so we don't need to explicitly
|
|
634
|
+
# creat new objects for them. Additionally, its easier to do this here than
|
|
635
|
+
# in the individual classes.
|
|
636
|
+
remove_list: list[str] = [
|
|
637
|
+
"vis.DataRange()",
|
|
638
|
+
"luminarycloud.vis.primitives.Plane()",
|
|
639
|
+
"luminarycloud.vis.primitives.Box()",
|
|
640
|
+
]
|
|
641
|
+
# Remove entire lines containing any remove_list item
|
|
642
|
+
code_lines = code.splitlines()
|
|
643
|
+
filtered_lines = [
|
|
644
|
+
line
|
|
645
|
+
for line in code_lines
|
|
646
|
+
if not any(remove_item in line for remove_item in remove_list)
|
|
647
|
+
]
|
|
648
|
+
code = "\n".join(filtered_lines)
|
|
649
|
+
|
|
650
|
+
code += "\n"
|
|
651
|
+
code += "extract_output = extractor.create_extracts(name='extract data', description='lonerg description')\n"
|
|
652
|
+
code += "status = extract_output.wait()\n"
|
|
653
|
+
code += "if status == ExtractStatusType.COMPLETED:\n"
|
|
654
|
+
code += " extract_output.save_files('data_extracts_prefix', True)\n"
|
|
655
|
+
code += "else:\n"
|
|
656
|
+
code += " print('Data extraction failed ', status)\n"
|
|
657
|
+
|
|
658
|
+
return imports + code
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def list_data_extracts(solution: Solution) -> list[ExtractOutput]:
|
|
536
662
|
"""
|
|
537
663
|
Lists all previously created data extract associated with a project and a solution.
|
|
538
664
|
|
|
@@ -567,7 +693,7 @@ def list_data_extracts(solution: Solution) -> List[ExtractOutput]:
|
|
|
567
693
|
req.data_only = True
|
|
568
694
|
res: vis_pb2.ListExtractsResponse = get_default_client().ListExtracts(req)
|
|
569
695
|
|
|
570
|
-
results:
|
|
696
|
+
results: list[ExtractOutput] = []
|
|
571
697
|
for extract in res.extracts:
|
|
572
698
|
result = ExtractOutput(_InternalToken())
|
|
573
699
|
result._set_data(
|
|
@@ -582,3 +708,47 @@ def list_data_extracts(solution: Solution) -> List[ExtractOutput]:
|
|
|
582
708
|
results.append(result)
|
|
583
709
|
|
|
584
710
|
return results
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _spec_to_data_extractor(spec: vis_pb2.ExtractSpec) -> DataExtractor:
|
|
714
|
+
entity = spec.entity_type.WhichOneof("entity")
|
|
715
|
+
if entity == "simulation":
|
|
716
|
+
sim_id = SimulationID(spec.entity_type.simulation.id)
|
|
717
|
+
sim = get_simulation(sim_id)
|
|
718
|
+
sols = sim.list_solutions()
|
|
719
|
+
found = False
|
|
720
|
+
for sol in sols:
|
|
721
|
+
if sol.id == spec.entity_type.simulation.solution_id:
|
|
722
|
+
extractor = DataExtractor(sol)
|
|
723
|
+
found = True
|
|
724
|
+
break
|
|
725
|
+
if not found:
|
|
726
|
+
raise ValueError("Error: could not find the solution")
|
|
727
|
+
else:
|
|
728
|
+
raise ValueError("Error: only solutions are supported for data extraction")
|
|
729
|
+
|
|
730
|
+
try:
|
|
731
|
+
_ = extractor # check to see if this is bound
|
|
732
|
+
except NameError:
|
|
733
|
+
raise ValueError(f"Error: could not create scene from entity")
|
|
734
|
+
|
|
735
|
+
filter_ids: list[str] = []
|
|
736
|
+
for filter in spec.filters:
|
|
737
|
+
filter_ids.append(filter.id)
|
|
738
|
+
typ = filter.WhichOneof("value")
|
|
739
|
+
pfilter: DataExtract | None = None
|
|
740
|
+
if typ == "line_sample":
|
|
741
|
+
pfilter = LineSample("")
|
|
742
|
+
elif typ == "intersection_curve":
|
|
743
|
+
pfilter = IntersectionCurve("")
|
|
744
|
+
else:
|
|
745
|
+
# Don't complain about vis filters that are not data extracts.
|
|
746
|
+
# If the extractor has no filters, it will return an empty string
|
|
747
|
+
# from the to_code path.
|
|
748
|
+
continue
|
|
749
|
+
|
|
750
|
+
assert pfilter is not None, "Internal error: filter type not set"
|
|
751
|
+
pfilter._from_proto(filter)
|
|
752
|
+
extractor.add_data_extract(pfilter)
|
|
753
|
+
|
|
754
|
+
return extractor
|
luminarycloud/vis/filters.py
CHANGED
|
@@ -9,11 +9,9 @@ from luminarycloud.enum import (
|
|
|
9
9
|
import luminarycloud.enum.quantity_type as quantity_type
|
|
10
10
|
from .._proto.api.v0.luminarycloud.vis import vis_pb2
|
|
11
11
|
from abc import ABC, abstractmethod
|
|
12
|
-
import math
|
|
13
|
-
import dataclasses as dc
|
|
14
12
|
from .display import Field, DisplayAttributes
|
|
15
13
|
from typing import List, Any, cast
|
|
16
|
-
from .primitives import Box, Plane
|
|
14
|
+
from .primitives import Box, Plane, AABB
|
|
17
15
|
from .vis_util import generate_id
|
|
18
16
|
from .._helpers._code_representation import CodeRepr
|
|
19
17
|
|
|
@@ -126,8 +124,7 @@ class Slice(Filter):
|
|
|
126
124
|
vis_filter = vis_pb2.Filter()
|
|
127
125
|
vis_filter.id = self.id
|
|
128
126
|
vis_filter.name = self.name
|
|
129
|
-
vis_filter.slice.plane.
|
|
130
|
-
vis_filter.slice.plane.normal.CopyFrom(_to_vector3(self.plane.normal)._to_proto())
|
|
127
|
+
vis_filter.slice.plane.CopyFrom(self.plane._to_proto())
|
|
131
128
|
vis_filter.slice.project_vectors = self.project_vectors
|
|
132
129
|
return vis_filter
|
|
133
130
|
|
|
@@ -138,12 +135,8 @@ class Slice(Filter):
|
|
|
138
135
|
self.id = filter.id
|
|
139
136
|
self.name = filter.name
|
|
140
137
|
self.project_vectors = filter.slice.project_vectors
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
self.plane.origin = origin
|
|
144
|
-
normal = Vector3()
|
|
145
|
-
normal._from_proto(filter.slice.plane.normal)
|
|
146
|
-
self.plane.normal = normal
|
|
138
|
+
self.plane = Plane()
|
|
139
|
+
self.plane._from_proto(filter.slice.plane)
|
|
147
140
|
|
|
148
141
|
|
|
149
142
|
class Isosurface(Filter):
|
|
@@ -254,8 +247,7 @@ class PlaneClip(Filter):
|
|
|
254
247
|
vis_filter = vis_pb2.Filter()
|
|
255
248
|
vis_filter.id = self.id
|
|
256
249
|
vis_filter.name = self.name
|
|
257
|
-
vis_filter.clip.plane.
|
|
258
|
-
vis_filter.clip.plane.normal.CopyFrom(_to_vector3(self.plane.normal)._to_proto())
|
|
250
|
+
vis_filter.clip.plane.CopyFrom(self.plane._to_proto())
|
|
259
251
|
vis_filter.clip.inverted = self.inverted
|
|
260
252
|
return vis_filter
|
|
261
253
|
|
|
@@ -268,10 +260,8 @@ class PlaneClip(Filter):
|
|
|
268
260
|
raise TypeError(f"Expected 'plane clip', got {clip_typ}")
|
|
269
261
|
self.id = filter.id
|
|
270
262
|
self.name = filter.name
|
|
271
|
-
self.plane
|
|
272
|
-
self.plane.
|
|
273
|
-
self.plane.origin = Vector3()
|
|
274
|
-
self.plane.origin._from_proto(filter.clip.plane.origin)
|
|
263
|
+
self.plane = Plane()
|
|
264
|
+
self.plane._from_proto(filter.clip.plane)
|
|
275
265
|
self.inverted = filter.clip.inverted
|
|
276
266
|
|
|
277
267
|
|
|
@@ -315,14 +305,7 @@ class BoxClip(Filter):
|
|
|
315
305
|
vis_filter = vis_pb2.Filter()
|
|
316
306
|
vis_filter.id = self.id
|
|
317
307
|
vis_filter.name = self.name
|
|
318
|
-
vis_filter.clip.box.
|
|
319
|
-
vis_filter.clip.box.lengths.CopyFrom(_to_vector3(self.box.lengths)._to_proto())
|
|
320
|
-
# The api interface is in degrees but the backend needs radians
|
|
321
|
-
radians = _to_vector3(self.box.angles)
|
|
322
|
-
radians.x = radians.x * (math.pi / 180)
|
|
323
|
-
radians.y = radians.y * (math.pi / 180)
|
|
324
|
-
radians.z = radians.z * (math.pi / 180)
|
|
325
|
-
vis_filter.clip.box.angles.CopyFrom(radians._to_proto())
|
|
308
|
+
vis_filter.clip.box.CopyFrom(self.box._to_proto())
|
|
326
309
|
vis_filter.clip.inverted = self.inverted
|
|
327
310
|
return vis_filter
|
|
328
311
|
|
|
@@ -335,16 +318,8 @@ class BoxClip(Filter):
|
|
|
335
318
|
raise TypeError(f"Expected 'box', got {clip_typ}")
|
|
336
319
|
self.id = filter.id
|
|
337
320
|
self.name = filter.name
|
|
338
|
-
self.box
|
|
339
|
-
self.box.
|
|
340
|
-
self.box.lengths = Vector3()
|
|
341
|
-
self.box.lengths._from_proto(filter.clip.box.lengths)
|
|
342
|
-
self.box.angles = Vector3()
|
|
343
|
-
self.box.angles._from_proto(filter.clip.box.angles)
|
|
344
|
-
# Backend units are radians, convert back to degrees
|
|
345
|
-
self.box.angles.x = self.box.angles.x * (180 / math.pi)
|
|
346
|
-
self.box.angles.y = self.box.angles.y * (180 / math.pi)
|
|
347
|
-
self.box.angles.z = self.box.angles.z * (180 / math.pi)
|
|
321
|
+
self.box = Box()
|
|
322
|
+
self.box._from_proto(filter.clip.box)
|
|
348
323
|
self.inverted = filter.clip.inverted
|
|
349
324
|
|
|
350
325
|
|
|
@@ -1065,6 +1040,88 @@ class SurfaceLIC(Filter):
|
|
|
1065
1040
|
return code
|
|
1066
1041
|
|
|
1067
1042
|
|
|
1043
|
+
class SurfaceLICPlane(Filter):
|
|
1044
|
+
"""
|
|
1045
|
+
A Surface Line Integral Convolution (LIC) filter is used to depict the flow
|
|
1046
|
+
direction and structure of vector fields (such as velocity) on surfaces. It
|
|
1047
|
+
enhances the perception of complex flow patterns by convolving noise
|
|
1048
|
+
textures along streamlines, making it easier to visually interpret the
|
|
1049
|
+
behavior of fluid flow on boundaries or surfaces in a simulation.
|
|
1050
|
+
|
|
1051
|
+
This filter extracts a plane clipped by an axis-aligned bounding box (AABB) from
|
|
1052
|
+
the volume solution and computes the surface LIC on the plane.
|
|
1053
|
+
The surface LIC outputs the values as grayscale colors on the specified
|
|
1054
|
+
plane. When the display attributes quantity is not None, the field colors
|
|
1055
|
+
are blended with the grayscale colors.
|
|
1056
|
+
|
|
1057
|
+
.. warning:: This feature is experimental and may change or be removed in the future.
|
|
1058
|
+
|
|
1059
|
+
Attributes:
|
|
1060
|
+
-----------
|
|
1061
|
+
quantity: VisQuantity
|
|
1062
|
+
Specifies the field used to advect particles for the surface LIC.
|
|
1063
|
+
Default: VELOCITY
|
|
1064
|
+
contrast: float
|
|
1065
|
+
Contrast controls the contrast of the resuting surface LIC. Valid values
|
|
1066
|
+
are in the [0.2, 3.0] range. Lower values means less contrast and
|
|
1067
|
+
higher values mean more contrast. Default: 1
|
|
1068
|
+
plane: Plane
|
|
1069
|
+
The plane to extract from the volume solution.
|
|
1070
|
+
clip_box: AABB
|
|
1071
|
+
The axis-aligned bounding box (AABB) to clip the plane with. This is
|
|
1072
|
+
useful to limit the area of the plane to a specific region of interest.
|
|
1073
|
+
"""
|
|
1074
|
+
|
|
1075
|
+
def __init__(self, name: str = "") -> None:
|
|
1076
|
+
super().__init__(generate_id("surface-lic-"))
|
|
1077
|
+
self.name = name
|
|
1078
|
+
self.contrast: float = 1.0
|
|
1079
|
+
self.quantity: VisQuantity = VisQuantity.VELOCITY
|
|
1080
|
+
self.plane: Plane = Plane()
|
|
1081
|
+
self.clip_box: AABB = AABB()
|
|
1082
|
+
|
|
1083
|
+
def _to_proto(self) -> vis_pb2.Filter:
|
|
1084
|
+
vis_filter = vis_pb2.Filter()
|
|
1085
|
+
vis_filter.id = self.id
|
|
1086
|
+
vis_filter.name = self.name
|
|
1087
|
+
if not isinstance(self.quantity, VisQuantity):
|
|
1088
|
+
raise TypeError(f"Expected 'VisQuantity', got {type(self.quantity).__name__}")
|
|
1089
|
+
if self.quantity == VisQuantity.WALL_SHEAR_STRESS:
|
|
1090
|
+
raise ValueError(
|
|
1091
|
+
"SurfaceLICPlane: wall shear stress is 0 in the volume and will produce no data"
|
|
1092
|
+
)
|
|
1093
|
+
if not isinstance(self.contrast, (int, float)):
|
|
1094
|
+
raise TypeError(f"Expected 'int or float', got {type(self.contrast).__name__}")
|
|
1095
|
+
if self.contrast < 0.2 or self.contrast > 3.0:
|
|
1096
|
+
raise ValueError("SurfaceLICPlane: contrast must be between 0.2 and 3.0")
|
|
1097
|
+
if not isinstance(self.plane, Plane):
|
|
1098
|
+
raise TypeError(f"Expected 'Plane', got {type(self.plane).__name__}")
|
|
1099
|
+
if not isinstance(self.clip_box, AABB):
|
|
1100
|
+
raise TypeError(f"Expected 'AABB', got {type(self.clip_box).__name__}")
|
|
1101
|
+
|
|
1102
|
+
vis_filter.surface_lic.plane.plane.CopyFrom(self.plane._to_proto())
|
|
1103
|
+
vis_filter.surface_lic.plane.clip_box.CopyFrom(self.clip_box._to_proto())
|
|
1104
|
+
vis_filter.surface_lic.field.quantity_typ = self.quantity.value
|
|
1105
|
+
vis_filter.surface_lic.contrast = self.contrast
|
|
1106
|
+
return vis_filter
|
|
1107
|
+
|
|
1108
|
+
def _from_proto(self, filter: vis_pb2.Filter) -> None:
|
|
1109
|
+
typ = filter.WhichOneof("value")
|
|
1110
|
+
if typ != "surface_lic":
|
|
1111
|
+
raise TypeError(f"Expected 'surface lic', got {typ}")
|
|
1112
|
+
l_typ = filter.surface_lic.WhichOneof("lic_type")
|
|
1113
|
+
if l_typ != "plane":
|
|
1114
|
+
raise TypeError(f"Expected 'plane', got {l_typ}")
|
|
1115
|
+
self.id = filter.id
|
|
1116
|
+
self.name = filter.name
|
|
1117
|
+
self.contrast = filter.surface_lic.contrast
|
|
1118
|
+
self.plane._from_proto(filter.surface_lic.plane.plane)
|
|
1119
|
+
self.clip_box._from_proto(filter.surface_lic.plane.clip_box)
|
|
1120
|
+
if not quantity_type._is_vector(self.quantity):
|
|
1121
|
+
raise ValueError("SurfaceLICPlane: quantity must be a vector type")
|
|
1122
|
+
self.quantity = VisQuantity(filter.surface_lic.field.quantity_typ)
|
|
1123
|
+
|
|
1124
|
+
|
|
1068
1125
|
def _filter_to_obj_name(filter: Filter) -> str:
|
|
1069
1126
|
"""
|
|
1070
1127
|
Helper function to convert a filter to a code object name used in code gen.
|
|
@@ -1093,5 +1150,7 @@ def _filter_to_obj_name(filter: Filter) -> str:
|
|
|
1093
1150
|
return "surface_streamlines"
|
|
1094
1151
|
elif isinstance(filter, SurfaceLIC):
|
|
1095
1152
|
return "surface_lic"
|
|
1153
|
+
elif isinstance(filter, SurfaceLICPlane):
|
|
1154
|
+
return "surface_lic_plane"
|
|
1096
1155
|
else:
|
|
1097
1156
|
raise TypeError(f"Unknown filter type: {type(filter).__name__}")
|
luminarycloud/vis/primitives.py
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import dataclasses as dc
|
|
3
3
|
from luminarycloud.types import Vector3, Vector3Like
|
|
4
4
|
from .._helpers._code_representation import CodeRepr
|
|
5
|
+
from .._proto.api.v0.luminarycloud.vis import vis_pb2
|
|
6
|
+
from ..types.vector3 import _to_vector3
|
|
7
|
+
import math
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
@dc.dataclass
|
|
@@ -18,11 +21,23 @@ class Plane(CodeRepr):
|
|
|
18
21
|
normal: Vector3Like = dc.field(default_factory=lambda: Vector3(x=1, y=0, z=0))
|
|
19
22
|
"""The vector orthogonal to the plane. Default: [0,1,0]"""
|
|
20
23
|
|
|
24
|
+
def _to_proto(self) -> vis_pb2.Plane:
|
|
25
|
+
plane = vis_pb2.Plane()
|
|
26
|
+
plane.origin.CopyFrom(_to_vector3(self.origin)._to_proto())
|
|
27
|
+
plane.normal.CopyFrom(_to_vector3(self.normal)._to_proto())
|
|
28
|
+
return plane
|
|
29
|
+
|
|
30
|
+
def _from_proto(self, proto: vis_pb2.Plane) -> None:
|
|
31
|
+
self.origin = Vector3()
|
|
32
|
+
self.origin._from_proto(proto.origin)
|
|
33
|
+
self.normal = Vector3()
|
|
34
|
+
self.normal._from_proto(proto.normal)
|
|
35
|
+
|
|
21
36
|
|
|
22
37
|
@dc.dataclass
|
|
23
38
|
class Box(CodeRepr):
|
|
24
39
|
"""
|
|
25
|
-
This class defines a box used for
|
|
40
|
+
This class defines a box used for filters such as box clip.
|
|
26
41
|
|
|
27
42
|
.. warning:: This feature is experimental and may change or be removed in the future.
|
|
28
43
|
|
|
@@ -37,3 +52,65 @@ class Box(CodeRepr):
|
|
|
37
52
|
The rotation of the box specified in Euler angles (degrees) and applied
|
|
38
53
|
in XYZ ordering. Default: [0,0,0]
|
|
39
54
|
"""
|
|
55
|
+
|
|
56
|
+
def _to_proto(self) -> vis_pb2.Box:
|
|
57
|
+
box = vis_pb2.Box()
|
|
58
|
+
box.center.CopyFrom(_to_vector3(self.center)._to_proto())
|
|
59
|
+
box.lengths.CopyFrom(_to_vector3(self.lengths)._to_proto())
|
|
60
|
+
# The api interface is in degrees but the backend needs radians
|
|
61
|
+
radians = _to_vector3(self.angles)
|
|
62
|
+
radians.x = radians.x * (math.pi / 180)
|
|
63
|
+
radians.y = radians.y * (math.pi / 180)
|
|
64
|
+
radians.z = radians.z * (math.pi / 180)
|
|
65
|
+
box.angles.CopyFrom(radians._to_proto())
|
|
66
|
+
return box
|
|
67
|
+
|
|
68
|
+
def _from_proto(self, proto: vis_pb2.Box) -> None:
|
|
69
|
+
center = Vector3()
|
|
70
|
+
center._from_proto(proto.center)
|
|
71
|
+
self.center = center
|
|
72
|
+
lengths = Vector3()
|
|
73
|
+
lengths._from_proto(proto.lengths)
|
|
74
|
+
self.lengths = lengths
|
|
75
|
+
self.angles = Vector3()
|
|
76
|
+
# Backend units are radians, convert back to degrees
|
|
77
|
+
self.angles.x = proto.angles.x * (180 / math.pi)
|
|
78
|
+
self.angles.y = proto.angles.y * (180 / math.pi)
|
|
79
|
+
self.angles.z = proto.angles.z * (180 / math.pi)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dc.dataclass
|
|
83
|
+
class AABB(CodeRepr):
|
|
84
|
+
"""
|
|
85
|
+
This class defines an axis-aligned bounding box used for filters such as SurfaceLICPlane.
|
|
86
|
+
|
|
87
|
+
.. warning:: This feature is experimental and may change or be removed in the future.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
min: Vector3Like = dc.field(default_factory=lambda: Vector3(x=0, y=0, z=0))
|
|
92
|
+
"""The min point of the axis-aligned box. Default: [0,0,0]."""
|
|
93
|
+
max: Vector3Like = dc.field(default_factory=lambda: Vector3(x=1, y=1, z=1))
|
|
94
|
+
"""The max point of the axis-aligned box. Default: [1,1,1]."""
|
|
95
|
+
|
|
96
|
+
def is_valid(self) -> bool:
|
|
97
|
+
"""
|
|
98
|
+
Check if the AABB is valid. An AABB is valid if min is less than max in all dimensions.
|
|
99
|
+
"""
|
|
100
|
+
min = _to_vector3(self.min)
|
|
101
|
+
max = _to_vector3(self.max)
|
|
102
|
+
return min.x <= max.x and min.y <= max.y and min.z <= max.z
|
|
103
|
+
|
|
104
|
+
def _to_proto(self) -> vis_pb2.AABB:
|
|
105
|
+
aabb = vis_pb2.AABB()
|
|
106
|
+
aabb.min.CopyFrom(_to_vector3(self.min)._to_proto())
|
|
107
|
+
aabb.max.CopyFrom(_to_vector3(self.max)._to_proto())
|
|
108
|
+
return aabb
|
|
109
|
+
|
|
110
|
+
def _from_proto(self, proto: vis_pb2.AABB) -> None:
|
|
111
|
+
min_point = Vector3()
|
|
112
|
+
min_point._from_proto(proto.min)
|
|
113
|
+
self.min = min_point
|
|
114
|
+
max_point = Vector3()
|
|
115
|
+
max_point._from_proto(proto.max)
|
|
116
|
+
self.max = max_point
|