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.
Files changed (59) hide show
  1. luminarycloud/_auth/auth.py +23 -34
  2. luminarycloud/_client/client.py +21 -5
  3. luminarycloud/_client/retry_interceptor.py +7 -0
  4. luminarycloud/_helpers/_create_geometry.py +0 -2
  5. luminarycloud/_helpers/_wait_for_mesh.py +14 -4
  6. luminarycloud/_helpers/warnings/__init__.py +0 -1
  7. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +120 -120
  8. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +2 -8
  9. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.py +25 -3
  10. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.pyi +30 -0
  11. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2_grpc.py +34 -0
  12. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2_grpc.pyi +12 -0
  13. luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2.py +97 -0
  14. luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2.pyi +93 -0
  15. luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2_grpc.py +132 -0
  16. luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2_grpc.pyi +44 -0
  17. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.py +88 -34
  18. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.pyi +96 -6
  19. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.py +68 -0
  20. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.pyi +24 -0
  21. luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.py +153 -133
  22. luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.pyi +51 -3
  23. luminarycloud/_proto/cad/shape_pb2.py +78 -19
  24. luminarycloud/_proto/cad/transformation_pb2.py +34 -15
  25. luminarycloud/_proto/geometry/geometry_pb2.py +62 -62
  26. luminarycloud/_proto/geometry/geometry_pb2.pyi +3 -5
  27. luminarycloud/_proto/hexmesh/hexmesh_pb2.py +17 -4
  28. luminarycloud/_proto/hexmesh/hexmesh_pb2.pyi +22 -1
  29. luminarycloud/_proto/quantity/quantity_pb2.py +19 -19
  30. luminarycloud/_proto/upload/upload_pb2.py +25 -15
  31. luminarycloud/_proto/upload/upload_pb2.pyi +31 -2
  32. luminarycloud/feature_modification.py +0 -5
  33. luminarycloud/geometry.py +15 -2
  34. luminarycloud/mesh.py +16 -0
  35. luminarycloud/named_variable_set.py +3 -4
  36. luminarycloud/outputs/stopping_conditions.py +0 -3
  37. luminarycloud/physics_ai/architectures.py +0 -4
  38. luminarycloud/physics_ai/inference.py +0 -4
  39. luminarycloud/physics_ai/models.py +0 -4
  40. luminarycloud/physics_ai/solution.py +2 -2
  41. luminarycloud/pipelines/__init__.py +6 -0
  42. luminarycloud/pipelines/arguments.py +105 -0
  43. luminarycloud/pipelines/core.py +204 -20
  44. luminarycloud/pipelines/operators.py +11 -9
  45. luminarycloud/pipelines/parameters.py +25 -4
  46. luminarycloud/project.py +37 -11
  47. luminarycloud/simulation_param.py +0 -2
  48. luminarycloud/simulation_template.py +1 -3
  49. luminarycloud/solution.py +1 -3
  50. luminarycloud/vis/__init__.py +2 -0
  51. luminarycloud/vis/data_extraction.py +201 -31
  52. luminarycloud/vis/filters.py +94 -35
  53. luminarycloud/vis/primitives.py +78 -1
  54. luminarycloud/vis/visualization.py +44 -6
  55. luminarycloud/volume_selection.py +0 -4
  56. {luminarycloud-0.16.1.dist-info → luminarycloud-0.17.0.dist-info}/METADATA +1 -1
  57. {luminarycloud-0.16.1.dist-info → luminarycloud-0.17.0.dist-info}/RECORD +58 -54
  58. luminarycloud/_helpers/warnings/experimental.py +0 -48
  59. {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 List, Tuple, cast, Union
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, get_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 ..exceptions import NotFoundError
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: List[str] = []
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) -> List[str]:
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.origin.CopyFrom(
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) -> List[Tuple[List[List[Union[str, int, float]]], str]]:
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: List[Tuple[List[List[Union[str, int, float]]], str]] = []
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: List[Union[str, float, int]] = []
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: List[List[Union[str, float, int]]] = []
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: List[List[Union[str, float, int]]] = []
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: List[Union[str, float, int]] = []
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: List[Tuple[str, str]] = []
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: List[DataExtract] = []
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: List[str] = []
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: List[str] = []
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: List[str] = []
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: List[str]) -> List[str]:
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: List[str] = []
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) -> List[str]:
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) -> List[str]:
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
- def list_data_extracts(solution: Solution) -> List[ExtractOutput]:
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: List[ExtractOutput] = []
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
@@ -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.origin.CopyFrom(_to_vector3(self.plane.origin)._to_proto())
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
- origin = Vector3()
142
- origin._from_proto(filter.slice.plane.origin)
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.origin.CopyFrom(_to_vector3(self.plane.origin)._to_proto())
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.normal = Vector3()
272
- self.plane.normal._from_proto(filter.clip.plane.normal)
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.center.CopyFrom(_to_vector3(self.box.center)._to_proto())
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.center = Vector3()
339
- self.box.center._from_proto(filter.clip.box.center)
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__}")
@@ -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 filter such as box clip.
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