luminarycloud 0.20.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 +5 -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/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 +33 -20
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.pyi +21 -1
- 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 +72 -3
- 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 +68 -19
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.pyi +98 -0
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2_grpc.py +33 -0
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2_grpc.pyi +10 -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 +490 -348
- luminarycloud/_proto/client/simulation_pb2.pyi +570 -8
- 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/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/feature_modification.py +45 -35
- luminarycloud/geometry.py +104 -8
- luminarycloud/geometry_version.py +57 -3
- luminarycloud/meshing/mesh_generation_params.py +8 -8
- luminarycloud/params/enum/_enum_wrappers.py +537 -30
- luminarycloud/params/simulation/adaptive_mesh_refinement_.py +4 -0
- luminarycloud/params/simulation/physics/__init__.py +0 -1
- luminarycloud/params/simulation/physics/periodic_pair_.py +12 -31
- luminarycloud/physics_ai/architectures.py +5 -5
- luminarycloud/physics_ai/inference.py +13 -13
- luminarycloud/physics_ai/solution.py +3 -1
- luminarycloud/pipelines/__init__.py +11 -3
- luminarycloud/pipelines/api.py +240 -4
- 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 +66 -3
- luminarycloud/simulation_param.py +0 -9
- luminarycloud/types/matrix3.py +12 -0
- luminarycloud/vis/__init__.py +2 -0
- luminarycloud/vis/interactive_report.py +79 -93
- luminarycloud/vis/report.py +219 -65
- luminarycloud/vis/visualization.py +60 -0
- luminarycloud/volume_selection.py +132 -69
- {luminarycloud-0.20.0.dist-info → luminarycloud-0.22.0.dist-info}/METADATA +1 -1
- {luminarycloud-0.20.0.dist-info → luminarycloud-0.22.0.dist-info}/RECORD +90 -89
- 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.20.0.dist-info → luminarycloud-0.22.0.dist-info}/WHEEL +0 -0
luminarycloud/vis/report.py
CHANGED
|
@@ -1,19 +1,162 @@
|
|
|
1
|
-
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .visualization import Scene, RenderOutput, range_query
|
|
2
6
|
from .data_extraction import DataExtractor, ExtractOutput
|
|
3
|
-
from ..enum import RenderStatusType, ExtractStatusType
|
|
7
|
+
from ..enum import RenderStatusType, ExtractStatusType, FieldAssociation
|
|
4
8
|
from ..solution import Solution
|
|
9
|
+
from .vis_util import _InternalToken, _get_status
|
|
5
10
|
from time import sleep
|
|
11
|
+
from .._proto.api.v0.luminarycloud.vis import vis_pb2
|
|
12
|
+
from .._client import get_default_client
|
|
13
|
+
from .._helpers._get_project_id import _get_project_id
|
|
14
|
+
import luminarycloud.enum.quantity_type as quantity_type
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .interactive_report import InteractiveReport
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# TODO Will/Matt: this could be something like what we store in the DB
|
|
21
|
+
# A report can contain a list of report entries that reference post proc.
|
|
22
|
+
# extracts + styling info for how they should be displayed
|
|
23
|
+
class ReportEntry:
|
|
24
|
+
"""
|
|
25
|
+
A single entry in a report, containing references to extracts and metadata.
|
|
26
|
+
Each extract can have multiple pieces of data (e.g. multiple images for a
|
|
27
|
+
RenderOutput, or multiple curves for an ExtractOutput). Metadata is a
|
|
28
|
+
dictionary of key/value pairs that can be used to store additional
|
|
29
|
+
information about the report entry. Typically, the metadata would include
|
|
30
|
+
things like simulation ID, lift/drag values, and scalar ranges for each
|
|
31
|
+
solution. The metadata is used to filter and sort data in the ensemble
|
|
32
|
+
widget.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self, project_id: str, extract_ids: list[str] = [], metadata: dict[str, str | float] = {}
|
|
37
|
+
) -> None:
|
|
38
|
+
self._project_id = project_id
|
|
39
|
+
self._extract_ids = extract_ids
|
|
40
|
+
self._extracts: list[ExtractOutput | RenderOutput] = []
|
|
41
|
+
self._metadata = metadata
|
|
42
|
+
self._statuses = statuses = [RenderStatusType.INVALID] * len(self._extract_ids)
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
return {
|
|
46
|
+
"project_id": self._project_id,
|
|
47
|
+
"extract_ids": self._extract_ids,
|
|
48
|
+
"metadata": self._metadata,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_dict(cls, data: dict) -> "ReportEntry":
|
|
53
|
+
return cls(
|
|
54
|
+
project_id=data["project_id"],
|
|
55
|
+
extract_ids=data.get("extract_ids", []),
|
|
56
|
+
metadata=data.get("metadata", {}),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def refresh_statuses(self) -> None:
|
|
60
|
+
for i, eid in enumerate(self._extract_ids):
|
|
61
|
+
self._statuses[i] = _get_status(self._project_id, eid)
|
|
62
|
+
|
|
63
|
+
def is_complete(self) -> bool:
|
|
64
|
+
self.refresh_statuses()
|
|
65
|
+
return all(
|
|
66
|
+
(status == RenderStatusType.COMPLETED or status == RenderStatusType.FAILED)
|
|
67
|
+
for status in self._statuses
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Download all extracts for this report entry
|
|
71
|
+
def download_extracts(self) -> None:
|
|
72
|
+
self._extracts = []
|
|
73
|
+
for eid in self._extract_ids:
|
|
74
|
+
status = _get_status(self._project_id, eid)
|
|
75
|
+
if status != ExtractStatusType.COMPLETED:
|
|
76
|
+
raise Exception(f"Extract {eid} is not complete")
|
|
77
|
+
req = vis_pb2.DownloadExtractRequest()
|
|
78
|
+
req.extract_id = eid
|
|
79
|
+
req.project_id = self._project_id
|
|
80
|
+
# TODO: This is a bit awkward in that we download the extract to figure out what type
|
|
81
|
+
# it is, but this is just a temporary thing, later we'll have a report DB table that
|
|
82
|
+
# stores the extracts for a report and their types, etc.
|
|
83
|
+
res: vis_pb2.DownloadExtractResponse = get_default_client().DownloadExtract(req)
|
|
84
|
+
extract = (
|
|
85
|
+
ExtractOutput(_InternalToken())
|
|
86
|
+
if res.HasField("line_data")
|
|
87
|
+
else RenderOutput(_InternalToken())
|
|
88
|
+
)
|
|
89
|
+
extract._set_data(eid, self._project_id, eid, eid, status)
|
|
90
|
+
self._extracts.append(extract)
|
|
6
91
|
|
|
7
92
|
|
|
8
|
-
# Notes(matt): we need a good way to pass "legend" information to the report.
|
|
9
|
-
# The legend is list of scalar values that are associated with each solution in
|
|
10
|
-
# the report. Examples include outputs like lift or drag, scalar ranges in the
|
|
11
|
-
# solutions, or any user provided data. We could add a helper class to auto-produce
|
|
12
|
-
# the legend data for common use cases or the user could provide their own. The data
|
|
13
|
-
# would look like a csv file or a dictionary keyed on the solution/sim id, where each
|
|
14
|
-
# entry is a list of scalar values. We would also need a header to describe what the values
|
|
15
|
-
# are.
|
|
16
93
|
class Report:
|
|
94
|
+
"""
|
|
95
|
+
A report containing multiple report entries. There is support for
|
|
96
|
+
serialization and deserialization to/from JSON since generating the extracts
|
|
97
|
+
and metadata can be expensive.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, entries: list[ReportEntry]):
|
|
101
|
+
self._entries = entries
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict:
|
|
104
|
+
return {"entries": [entry.to_dict() for entry in self._entries]}
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, data: dict) -> "Report":
|
|
108
|
+
entries = [ReportEntry.from_dict(e) for e in data.get("entries", [])]
|
|
109
|
+
return cls(entries)
|
|
110
|
+
|
|
111
|
+
def _check_status(self) -> bool:
|
|
112
|
+
"""Check the status of all ReportEntries and their extracts, grouped by entry."""
|
|
113
|
+
still_pending = False
|
|
114
|
+
print("\n" + "=" * 60)
|
|
115
|
+
print("STATUS CHECK".center(60))
|
|
116
|
+
print("=" * 60)
|
|
117
|
+
|
|
118
|
+
if not self._entries:
|
|
119
|
+
raise RuntimeError("No report entries to check status.")
|
|
120
|
+
|
|
121
|
+
print(f"{'Entry':<8} | {'Extract ID':<20} | {'Status':<15}")
|
|
122
|
+
print("-" * 60)
|
|
123
|
+
for idx, entry in enumerate(self._entries):
|
|
124
|
+
entry.refresh_statuses()
|
|
125
|
+
for eid, status in zip(entry._extract_ids, entry._statuses):
|
|
126
|
+
if status != RenderStatusType.COMPLETED and status != RenderStatusType.FAILED:
|
|
127
|
+
still_pending = True
|
|
128
|
+
print(f"{idx:<8} | {eid:<20} | {status.name:<15}")
|
|
129
|
+
print("=" * 60)
|
|
130
|
+
return still_pending
|
|
131
|
+
|
|
132
|
+
def wait_for_completion(self):
|
|
133
|
+
"""Wait for all report entries' extracts to complete."""
|
|
134
|
+
if not self._entries:
|
|
135
|
+
raise RuntimeError("No report entries to wait for.")
|
|
136
|
+
while self._check_status():
|
|
137
|
+
sleep(5)
|
|
138
|
+
print("All report entries' extracts have completed.")
|
|
139
|
+
|
|
140
|
+
def interact(self) -> "InteractiveReport":
|
|
141
|
+
from .interactive_report import InteractiveReport
|
|
142
|
+
|
|
143
|
+
if not self._check_status:
|
|
144
|
+
raise ValueError("Error: report entries are still pending")
|
|
145
|
+
return InteractiveReport(self._entries)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ReportGenerator:
|
|
149
|
+
"""
|
|
150
|
+
A helper for generating reports from multiple solutions, scenes, data extractors and
|
|
151
|
+
per solution metatdata.
|
|
152
|
+
|
|
153
|
+
Attributes:
|
|
154
|
+
-----------
|
|
155
|
+
calculate_ranges: bool
|
|
156
|
+
Whether to auto-calculate solution quantity ranges and add them to the
|
|
157
|
+
metadata. Default is False.
|
|
158
|
+
"""
|
|
159
|
+
|
|
17
160
|
def __init__(self, solutions: list[Solution]):
|
|
18
161
|
self._scenes: list[Scene] = []
|
|
19
162
|
self._data_extractors: list[DataExtractor] = []
|
|
@@ -21,6 +164,10 @@ class Report:
|
|
|
21
164
|
# When we fire off requests we use these objects to track the progress.
|
|
22
165
|
self._extract_outputs: list[ExtractOutput] = []
|
|
23
166
|
self._render_outputs: list[RenderOutput] = []
|
|
167
|
+
# Controls if we should calculate solution quanity ranges
|
|
168
|
+
self.calculate_ranges: bool = False
|
|
169
|
+
# Key is solution ID, value is the metadata dict
|
|
170
|
+
self._metadata: dict[str, dict[str, str | float]] = {}
|
|
24
171
|
for solution in solutions:
|
|
25
172
|
if not isinstance(solution, Solution):
|
|
26
173
|
raise TypeError("Expected a list of Solution objects.")
|
|
@@ -30,69 +177,76 @@ class Report:
|
|
|
30
177
|
raise TypeError("Expected a Scene object.")
|
|
31
178
|
self._scenes.append(scene)
|
|
32
179
|
|
|
180
|
+
# TODO(Matt): we could just make this a single data extract then control how they
|
|
181
|
+
# are added to each solution.
|
|
33
182
|
def add_data_extractor(self, data_extractor: DataExtractor):
|
|
34
183
|
if not isinstance(data_extractor, DataExtractor):
|
|
35
184
|
raise TypeError("Expected a DataExtractor object.")
|
|
36
185
|
self._data_extractors.append(data_extractor)
|
|
37
186
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
print("STATUS CHECK".center(60))
|
|
43
|
-
print("=" * 60)
|
|
44
|
-
|
|
45
|
-
if not self._render_outputs and not self._extract_outputs:
|
|
46
|
-
raise RuntimeError("No render outputs or extract outputs to check status.")
|
|
47
|
-
|
|
48
|
-
# Check render outputs
|
|
49
|
-
if self._render_outputs:
|
|
50
|
-
print(f"{'Type':<8} | {'ID':<20} | {'Status':<15}")
|
|
51
|
-
print("-" * 60)
|
|
52
|
-
|
|
53
|
-
for output in self._render_outputs:
|
|
54
|
-
if (
|
|
55
|
-
output.status != RenderStatusType.COMPLETED
|
|
56
|
-
and output.status != RenderStatusType.FAILED
|
|
57
|
-
):
|
|
58
|
-
output.refresh()
|
|
59
|
-
still_pending = True
|
|
60
|
-
print(f"{'Render':<8} | {str(output._extract_id):<20} | {output.status.name:<15}")
|
|
61
|
-
|
|
62
|
-
# Check extract outputs
|
|
63
|
-
for output in self._extract_outputs:
|
|
64
|
-
if (
|
|
65
|
-
output.status != ExtractStatusType.COMPLETED
|
|
66
|
-
and output.status != ExtractStatusType.FAILED
|
|
67
|
-
):
|
|
68
|
-
output.refresh()
|
|
69
|
-
still_pending = True
|
|
70
|
-
print(f"{'Extract':<8} | {str(output._extract_id):<20} | {output.status.name:<15}")
|
|
71
|
-
|
|
72
|
-
print("=" * 60)
|
|
73
|
-
return still_pending
|
|
187
|
+
def add_metadata(self, solution_id: str, metadata: dict[str, str | float]):
|
|
188
|
+
if solution_id not in self._metadata:
|
|
189
|
+
self._metadata[solution_id] = {}
|
|
190
|
+
self._metadata[solution_id].update(metadata)
|
|
74
191
|
|
|
75
|
-
def
|
|
192
|
+
def create_report(self) -> Report:
|
|
193
|
+
entries = []
|
|
76
194
|
for solution in self._solution:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
195
|
+
extract_ids = []
|
|
196
|
+
project_id = _get_project_id(solution)
|
|
197
|
+
if not project_id:
|
|
198
|
+
raise ValueError("Solution does not have a project_id.")
|
|
199
|
+
metadata = self._metadata.get(solution.id, {})
|
|
200
|
+
metadata["solution id"] = solution.id
|
|
201
|
+
if self.calculate_ranges:
|
|
202
|
+
print(f"Calculating solution quantity ranges {solution.id}")
|
|
203
|
+
ranges = range_query(solution, FieldAssociation.CELLS)
|
|
204
|
+
for range_res in ranges:
|
|
205
|
+
if not quantity_type._is_vector(range_res.quantity):
|
|
206
|
+
metadata[f"{range_res.field_name} min"] = range_res.ranges[0].min_value
|
|
207
|
+
metadata[f"{range_res.field_name} max"] = range_res.ranges[0].max_value
|
|
208
|
+
else:
|
|
209
|
+
for r in range(len(range_res.ranges)):
|
|
210
|
+
if r == 0:
|
|
211
|
+
comp = "x"
|
|
212
|
+
elif r == 1:
|
|
213
|
+
comp = "y"
|
|
214
|
+
elif r == 2:
|
|
215
|
+
comp = "z"
|
|
216
|
+
elif r == 3:
|
|
217
|
+
comp = "mag"
|
|
218
|
+
comp_range = range_res.ranges[r]
|
|
219
|
+
metadata[f"{range_res.field_name} min ({comp})"] = comp_range.min_value
|
|
220
|
+
metadata[f"{range_res.field_name} max ({comp})"] = comp_range.max_value
|
|
84
221
|
for extractor in self._data_extractors:
|
|
85
222
|
sol_extractor = extractor.clone(solution)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
name="Report Extract", description="Generated Report Extract"
|
|
89
|
-
)
|
|
223
|
+
extract = sol_extractor.create_extracts(
|
|
224
|
+
name="Report Extract", description="Generated Report Extract"
|
|
90
225
|
)
|
|
226
|
+
extract_ids.append(extract._extract_id)
|
|
91
227
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
228
|
+
for scene in self._scenes:
|
|
229
|
+
sol_scene = scene.clone(solution)
|
|
230
|
+
render_extract = sol_scene.render_images(
|
|
231
|
+
name="Report Scene", description="Generated Report Scene"
|
|
232
|
+
)
|
|
233
|
+
extract_ids.append(render_extract._extract_id)
|
|
234
|
+
|
|
235
|
+
entries.append(ReportEntry(project_id, extract_ids, metadata))
|
|
236
|
+
return Report(entries)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def save_report_to_json(report: Report, name: str, directory: str = ".") -> str:
|
|
240
|
+
"""Save a Report object to a JSON file named {name}_lcreport.json in the specified directory."""
|
|
241
|
+
filename = f"{name}_lcreport.json"
|
|
242
|
+
filepath = os.path.join(directory, filename)
|
|
243
|
+
with open(filepath, "w") as f:
|
|
244
|
+
json.dump(report.to_dict(), f, indent=2)
|
|
245
|
+
return filepath
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_report_from_json(filepath: str) -> "Report":
|
|
249
|
+
"""Load a Report object from a JSON file at the given file path."""
|
|
250
|
+
with open(filepath, "r") as f:
|
|
251
|
+
data = json.load(f)
|
|
252
|
+
return Report.from_dict(data)
|
|
@@ -24,6 +24,7 @@ from luminarycloud.enum import (
|
|
|
24
24
|
SceneMode,
|
|
25
25
|
VisQuantity,
|
|
26
26
|
QuantityType,
|
|
27
|
+
FieldAssociation,
|
|
27
28
|
)
|
|
28
29
|
from ..exceptions import NotFoundError
|
|
29
30
|
from ..geometry import Geometry, get_geometry
|
|
@@ -1094,6 +1095,65 @@ def list_quantities(solution: Solution) -> List[VisQuantity]:
|
|
|
1094
1095
|
return result
|
|
1095
1096
|
|
|
1096
1097
|
|
|
1098
|
+
@dc.dataclass
|
|
1099
|
+
class RangeResult:
|
|
1100
|
+
ranges: List[DataRange]
|
|
1101
|
+
"""
|
|
1102
|
+
A list of ranges per component. Scalars have a single range and vector ranges are in
|
|
1103
|
+
in x,y,z, magnitude order.
|
|
1104
|
+
"""
|
|
1105
|
+
quantity: VisQuantity
|
|
1106
|
+
""" The quantity type for the field, if available. """
|
|
1107
|
+
field_name: str
|
|
1108
|
+
""" Name of the field. """
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def range_query(solution: Solution, field_association: FieldAssociation) -> List[RangeResult]:
|
|
1112
|
+
"""
|
|
1113
|
+
The range query returns the min/max values for all fields in a solution. Two
|
|
1114
|
+
types of ranges can be chosen: cell-centered and point-centered data. The
|
|
1115
|
+
results will vary based on the choice. The solver natively outputs cell-centered
|
|
1116
|
+
data, so the cell based query will return the actual min/max values
|
|
1117
|
+
from the solver run. Visualization operates on point-centered data, which is
|
|
1118
|
+
recentered from the cell-centered data.
|
|
1119
|
+
|
|
1120
|
+
Parameters
|
|
1121
|
+
----------
|
|
1122
|
+
solution: Solution
|
|
1123
|
+
The the solution object to query.
|
|
1124
|
+
field_association: FieldAssociation
|
|
1125
|
+
The type of data to query, either cell-centered or point-centered.
|
|
1126
|
+
"""
|
|
1127
|
+
if not isinstance(solution, Solution):
|
|
1128
|
+
raise TypeError(f"Expected 'Solution', got {type(solution).__name__}")
|
|
1129
|
+
|
|
1130
|
+
if not isinstance(field_association, FieldAssociation):
|
|
1131
|
+
raise TypeError(f"Expected 'FieldAssociation', got {type(field_association).__name__}")
|
|
1132
|
+
|
|
1133
|
+
sim = get_simulation(solution.simulation_id)
|
|
1134
|
+
req = vis_pb2.RangeQueryRequest()
|
|
1135
|
+
req.entity.simulation.id = solution.simulation_id
|
|
1136
|
+
req.entity.simulation.solution_id = solution.id
|
|
1137
|
+
req.project_id = sim.project_id
|
|
1138
|
+
if field_association == FieldAssociation.POINTS:
|
|
1139
|
+
req.field_association = vis_pb2.FieldAssociation.FIELD_ASSOCIATION_POINTS
|
|
1140
|
+
else:
|
|
1141
|
+
req.field_association = vis_pb2.FieldAssociation.FIELD_ASSOCIATION_CELLS
|
|
1142
|
+
res: vis_pb2.RangeQueryReply = get_default_client().RangeQuery(req)
|
|
1143
|
+
result: List[RangeResult] = []
|
|
1144
|
+
for r in res.range:
|
|
1145
|
+
ranges = []
|
|
1146
|
+
for r_range in r.range:
|
|
1147
|
+
data_range = DataRange()
|
|
1148
|
+
data_range.min_value = r_range.min
|
|
1149
|
+
data_range.max_value = r_range.max
|
|
1150
|
+
ranges.append(data_range)
|
|
1151
|
+
result.append(
|
|
1152
|
+
RangeResult(ranges=ranges, quantity=VisQuantity(r.quantity), field_name=r.field_name)
|
|
1153
|
+
)
|
|
1154
|
+
return result
|
|
1155
|
+
|
|
1156
|
+
|
|
1097
1157
|
def list_renders(entity: Geometry | Mesh | Solution) -> List[RenderOutput]:
|
|
1098
1158
|
"""
|
|
1099
1159
|
Lists all previously created renders associated with a project and an entity.
|