luminarycloud 0.21.2__py3-none-any.whl → 0.22.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. luminarycloud/__init__.py +3 -1
  2. luminarycloud/_client/authentication_plugin.py +49 -0
  3. luminarycloud/_client/client.py +38 -8
  4. luminarycloud/_client/http_client.py +1 -1
  5. luminarycloud/_client/retry_interceptor.py +64 -2
  6. luminarycloud/_feature_flag.py +22 -0
  7. luminarycloud/_helpers/_create_simulation.py +7 -2
  8. luminarycloud/_helpers/download.py +11 -0
  9. luminarycloud/_helpers/proto_decorator.py +13 -5
  10. luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2.py +55 -0
  11. luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2.pyi +52 -0
  12. luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2_grpc.py +72 -0
  13. luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2_grpc.pyi +35 -0
  14. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +132 -132
  15. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +36 -8
  16. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.py +74 -73
  17. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.pyi +8 -1
  18. luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.py +53 -23
  19. luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.pyi +54 -1
  20. luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2.py +195 -0
  21. luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2.pyi +361 -0
  22. luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2_grpc.py +172 -0
  23. luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2_grpc.pyi +66 -0
  24. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.py +97 -61
  25. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.pyi +68 -3
  26. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.py +34 -0
  27. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.pyi +12 -0
  28. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.py +33 -31
  29. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.pyi +23 -2
  30. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.py +88 -65
  31. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.pyi +42 -0
  32. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.py +34 -0
  33. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.pyi +12 -0
  34. luminarycloud/_proto/base/base_pb2.py +7 -6
  35. luminarycloud/_proto/base/base_pb2.pyi +4 -0
  36. luminarycloud/_proto/cad/shape_pb2.py +39 -19
  37. luminarycloud/_proto/cad/shape_pb2.pyi +86 -34
  38. luminarycloud/_proto/client/simulation_pb2.py +3 -3
  39. luminarycloud/_proto/geometry/geometry_pb2.py +77 -63
  40. luminarycloud/_proto/geometry/geometry_pb2.pyi +42 -3
  41. luminarycloud/_proto/hexmesh/hexmesh_pb2.py +22 -18
  42. luminarycloud/_proto/hexmesh/hexmesh_pb2.pyi +18 -2
  43. luminarycloud/_proto/physicsaiinferenceservice/physicsaiinferenceservice_pb2.py +30 -0
  44. luminarycloud/_proto/physicsaiinferenceservice/physicsaiinferenceservice_pb2.pyi +7 -0
  45. luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2.py +2 -2
  46. luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.py +34 -0
  47. luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.pyi +12 -0
  48. luminarycloud/enum/vis_enums.py +6 -0
  49. luminarycloud/feature_modification.py +32 -1
  50. luminarycloud/geometry.py +67 -7
  51. luminarycloud/geometry_version.py +4 -0
  52. luminarycloud/mesh.py +4 -0
  53. luminarycloud/meshing/mesh_generation_params.py +13 -14
  54. luminarycloud/meshing/sizing_strategy/sizing_strategies.py +1 -2
  55. luminarycloud/physics_ai/solution.py +4 -0
  56. luminarycloud/pipelines/api.py +99 -8
  57. luminarycloud/pipelines/core.py +12 -2
  58. luminarycloud/pipelines/stages.py +22 -9
  59. luminarycloud/project.py +5 -8
  60. luminarycloud/simulation.py +57 -0
  61. luminarycloud/types/vector3.py +1 -2
  62. luminarycloud/vis/data_extraction.py +7 -7
  63. luminarycloud/vis/interactive_report.py +163 -7
  64. luminarycloud/vis/report.py +113 -1
  65. luminarycloud/volume_selection.py +71 -7
  66. {luminarycloud-0.21.2.dist-info → luminarycloud-0.22.1.dist-info}/METADATA +1 -1
  67. {luminarycloud-0.21.2.dist-info → luminarycloud-0.22.1.dist-info}/RECORD +68 -58
  68. {luminarycloud-0.21.2.dist-info → luminarycloud-0.22.1.dist-info}/WHEEL +1 -1
  69. luminarycloud/pipeline_util/dictable.py +0 -27
@@ -73,6 +73,12 @@ class VisQuantity(IntEnum):
73
73
  Q_CRITERION_TIME_AVERAGE = quantitypb.Q_CRITERION_TIME_AVERAGE
74
74
  HEAT_FLUX_TIME_AVERAGE = quantitypb.HEAT_FLUX_TIME_AVERAGE
75
75
  DEBUG_QUANTITY = quantitypb.DEBUG_QUANTITY
76
+ # Actuator disk quanties
77
+ THRUST_PER_UNIT_AREA = quantitypb.THRUST_PER_UNIT_AREA
78
+ TORQUE_PER_UNIT_AREA = quantitypb.TORQUE_PER_UNIT_AREA
79
+ BLADE_LOCAL_ANGLE_OF_ATTACK = quantitypb.BLADE_LOCAL_ANGLE_OF_ATTACK
80
+ BLADE_SECTIONAL_DRAG_COEFFICIENT = quantitypb.BLADE_SECTIONAL_DRAG_COEFFICIENT
81
+ BLADE_SECTIONAL_LIFT_COEFFICIENT = quantitypb.BLADE_SECTIONAL_LIFT_COEFFICIENT
76
82
 
77
83
 
78
84
  # Return the text name for the VisQuantity including the units, as it appears in the UI
@@ -265,6 +265,7 @@ def modify_delete(
265
265
  def modify_union(
266
266
  feature: gpb.Feature,
267
267
  volumes: Optional[List[Volume | int]] = None,
268
+ keep: Optional[bool] = None,
268
269
  ) -> gpb.Modification:
269
270
  """
270
271
  Modify a boolean union feature with optional new body IDs.
@@ -273,6 +274,7 @@ def modify_union(
273
274
  Args:
274
275
  feature: A gpb.Feature object
275
276
  volumes: List of volumes or volume IDs to union
277
+ keep: Whether to keep the original bodies
276
278
 
277
279
  Returns:
278
280
  A gpb.Modification object
@@ -287,6 +289,9 @@ def modify_union(
287
289
  vol_ids = _volumes_to_int_list(volumes)
288
290
  _update_repeated_field(boolean_op.reg_union.bodies, vol_ids)
289
291
 
292
+ if keep is not None:
293
+ boolean_op.reg_union.keep_source_bodies = keep
294
+
290
295
  return gpb.Modification(
291
296
  mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
292
297
  feature=feature_copy,
@@ -297,6 +302,8 @@ def modify_subtraction(
297
302
  feature: gpb.Feature,
298
303
  volumes: Optional[List[Volume | int]] = None,
299
304
  tool_volumes: Optional[List[Volume | int]] = None,
305
+ keep_source_bodies: Optional[bool] = None,
306
+ keep_tool_bodies: Optional[bool] = None,
300
307
  propagate_tool_tags: Optional[bool] = None,
301
308
  ) -> gpb.Modification:
302
309
  """
@@ -307,6 +314,8 @@ def modify_subtraction(
307
314
  feature: A gpb.Feature object
308
315
  volumes: List of volumes or volume IDs to subtract from
309
316
  tool_volumes: List of volumes or volume IDs to use for subtraction
317
+ keep_source_bodies: Whether to keep the original bodies
318
+ keep_tool_bodies: Whether to keep the tool bodies
310
319
  propagate_tool_tags: Whether to propagate tool tags
311
320
 
312
321
  Returns:
@@ -325,6 +334,12 @@ def modify_subtraction(
325
334
  tool_ids = _volumes_to_int_list(tool_volumes)
326
335
  _update_repeated_field(boolean_op.reg_subtraction.tools, tool_ids)
327
336
 
337
+ if keep_source_bodies is not None:
338
+ boolean_op.reg_subtraction.keep_source_bodies = keep_source_bodies
339
+
340
+ if keep_tool_bodies is not None:
341
+ boolean_op.reg_subtraction.keep_tool_bodies = keep_tool_bodies
342
+
328
343
  if propagate_tool_tags is not None:
329
344
  boolean_op.reg_subtraction.propagate_tool_tags = propagate_tool_tags
330
345
 
@@ -335,7 +350,9 @@ def modify_subtraction(
335
350
 
336
351
 
337
352
  def modify_intersection(
338
- feature: gpb.Feature, volumes: Optional[List[Volume | int]] = None
353
+ feature: gpb.Feature,
354
+ volumes: Optional[List[Volume | int]] = None,
355
+ keep: Optional[bool] = None,
339
356
  ) -> gpb.Modification:
340
357
  """
341
358
  Modify a boolean intersection feature with optional new volumes.
@@ -344,6 +361,7 @@ def modify_intersection(
344
361
  Args:
345
362
  feature: A gpb.Feature object
346
363
  volumes: List of volumes or volume IDs to intersect
364
+ keep: Whether to keep the original bodies
347
365
 
348
366
  Returns:
349
367
  A gpb.Modification object
@@ -358,6 +376,9 @@ def modify_intersection(
358
376
  vol_ids = _volumes_to_int_list(volumes)
359
377
  _update_repeated_field(boolean_op.reg_intersection.bodies, vol_ids)
360
378
 
379
+ if keep is not None:
380
+ boolean_op.reg_intersection.keep_source_bodies = keep
381
+
361
382
  return gpb.Modification(
362
383
  mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
363
384
  feature=feature_copy,
@@ -368,6 +389,8 @@ def modify_chop(
368
389
  feature: gpb.Feature,
369
390
  volumes: Optional[List[Volume | int]] = None,
370
391
  tool_volumes: Optional[List[Volume | int]] = None,
392
+ keep_source_bodies: Optional[bool] = None,
393
+ keep_tool_bodies: Optional[bool] = None,
371
394
  propagate_tool_tags: Optional[bool] = None,
372
395
  ) -> gpb.Modification:
373
396
  """
@@ -378,6 +401,8 @@ def modify_chop(
378
401
  feature: A gpb.Feature object
379
402
  volumes: List of volumes or volume IDs to chop
380
403
  tool_volumes: List of volumes or volume IDs to use for chopping
404
+ keep_source_bodies: Whether to keep the original bodies
405
+ keep_tool_bodies: Whether to keep the tool bodies
381
406
  propagate_tool_tags: Whether to propagate tool tags
382
407
 
383
408
  Returns:
@@ -397,6 +422,12 @@ def modify_chop(
397
422
  tool_ids = _volumes_to_int_list(tool_volumes)
398
423
  _update_repeated_field(boolean_op.reg_chop.tools, tool_ids)
399
424
 
425
+ if keep_source_bodies is not None:
426
+ boolean_op.reg_chop.keep_source_bodies = keep_source_bodies
427
+
428
+ if keep_tool_bodies is not None:
429
+ boolean_op.reg_chop.keep_tool_bodies = keep_tool_bodies
430
+
400
431
  if propagate_tool_tags is not None:
401
432
  boolean_op.reg_chop.propagate_tool_tags = propagate_tool_tags
402
433
 
luminarycloud/geometry.py CHANGED
@@ -61,6 +61,10 @@ class Geometry(ProtoWrapperBase):
61
61
  """
62
62
  return timestamp_to_datetime(self._proto.update_time)
63
63
 
64
+ @property
65
+ def url(self) -> str:
66
+ return f"{self.project().url}/geometry/{self.id}"
67
+
64
68
  def project(self) -> "Project":
65
69
  """
66
70
  Get the project this geometry belongs to.
@@ -84,6 +88,18 @@ class Geometry(ProtoWrapperBase):
84
88
  res = get_default_client().UpdateGeometry(req)
85
89
  self._proto = res.geometry
86
90
 
91
+ def delete(self) -> None:
92
+ """
93
+ Delete the geometry.
94
+
95
+ This operation cannot be reverted and all the geometry data will be deleted as part of
96
+ this request.
97
+ """
98
+ req = geometrypb.DeleteGeometryRequest(
99
+ geometry_id=self.id,
100
+ )
101
+ get_default_client().DeleteGeometry(req)
102
+
87
103
  def copy(self, name: str = "") -> "Geometry":
88
104
  """
89
105
  Copy the geometry.
@@ -386,7 +402,7 @@ class Geometry(ProtoWrapperBase):
386
402
  jitter = random.uniform(0.5, 1.5)
387
403
  time.sleep(2 + jitter)
388
404
 
389
- def create_tag(self, name: str, entities: Sequence[Volume | Surface]) -> None:
405
+ def create_tag(self, name: str, entities: Sequence[Volume | Surface]) -> Tag:
390
406
  """
391
407
  Create a tag in the geometry.
392
408
 
@@ -396,6 +412,11 @@ class Geometry(ProtoWrapperBase):
396
412
  The name of the tag to create.
397
413
  entities : list of Volumes or Surfaces
398
414
  The Volumes and Surfaces to tag.
415
+
416
+ Returns
417
+ -------
418
+ Tag
419
+ The tag that was created.
399
420
  """
400
421
  volume_ids = []
401
422
  surface_ids = []
@@ -407,8 +428,7 @@ class Geometry(ProtoWrapperBase):
407
428
  else:
408
429
  raise TypeError("entities must be of type Volume or Surface")
409
430
 
410
- req = geometrypb.ModifyGeometryRequest(
411
- geometry_id=self.id,
431
+ self._modify(
412
432
  modification=gpb.Modification(
413
433
  mod_type=gpb.Modification.MODIFICATION_TYPE_CREATE_TAG,
414
434
  create_or_update_tag=gpb.CreateOrUpdateTag(
@@ -418,9 +438,9 @@ class Geometry(ProtoWrapperBase):
418
438
  ),
419
439
  ),
420
440
  )
421
- get_default_client().ModifyGeometry(req)
441
+ return self._get_tag_by_name(name)
422
442
 
423
- def rename_tag(self, old_name: str, new_name: str) -> None:
443
+ def rename_tag(self, old_name: str, new_name: str) -> Tag:
424
444
  """
425
445
  Rename a tag in the geometry.
426
446
 
@@ -430,6 +450,11 @@ class Geometry(ProtoWrapperBase):
430
450
  The name of the tag to rename.
431
451
  new_name : str
432
452
  The new name for the tag.
453
+
454
+ Returns
455
+ -------
456
+ Tag
457
+ The updated tag.
433
458
  """
434
459
  self._modify(
435
460
  modification=gpb.Modification(
@@ -440,8 +465,9 @@ class Geometry(ProtoWrapperBase):
440
465
  ),
441
466
  ),
442
467
  )
468
+ return self._get_tag_by_name(new_name)
443
469
 
444
- def untag_entities(self, name: str, entities: Sequence[Volume | Surface]) -> None:
470
+ def untag_entities(self, name: str, entities: Sequence[Volume | Surface]) -> Tag | None:
445
471
  """
446
472
  Untag entities from a tag in the geometry.
447
473
 
@@ -452,6 +478,11 @@ class Geometry(ProtoWrapperBase):
452
478
  entities : list of Volumes or Surfaces
453
479
  The Volumes and Surfaces to untag. If empty, all entities with the
454
480
  tag will be untagged.
481
+
482
+ Returns
483
+ -------
484
+ Tag
485
+ The updated tag.
455
486
  """
456
487
  volume_ids = []
457
488
  surface_ids = []
@@ -473,8 +504,12 @@ class Geometry(ProtoWrapperBase):
473
504
  ),
474
505
  ),
475
506
  )
507
+ try:
508
+ return self._get_tag_by_name(name)
509
+ except ValueError:
510
+ return None
476
511
 
477
- def update_tag(self, name: str, entities: Sequence[Volume | Surface]) -> None:
512
+ def update_tag(self, name: str, entities: Sequence[Volume | Surface]) -> Tag:
478
513
  """
479
514
  Adds entities to a tag in the geometry.
480
515
 
@@ -483,6 +518,11 @@ class Geometry(ProtoWrapperBase):
483
518
  name : str
484
519
  The name of the tag to update.
485
520
  entities : list of Volumes or Surfaces
521
+
522
+ Returns
523
+ -------
524
+ Tag
525
+ The updated tag.
486
526
  """
487
527
  volume_ids = []
488
528
  surface_ids = []
@@ -504,6 +544,7 @@ class Geometry(ProtoWrapperBase):
504
544
  ),
505
545
  ),
506
546
  )
547
+ return self._get_tag_by_name(name)
507
548
 
508
549
  def list_tags(self) -> list[Tag]:
509
550
  """
@@ -519,6 +560,25 @@ class Geometry(ProtoWrapperBase):
519
560
  res: geometrypb.ListTagsResponse = get_default_client().ListTags(req)
520
561
  return [Tag(t) for t in res.tags]
521
562
 
563
+ def _get_tag_by_name(self, name: str) -> Tag:
564
+ """
565
+ Get a specific tag from the geometry.
566
+
567
+ Parameters
568
+ ----------
569
+ name : str
570
+ The name of the tag.
571
+
572
+ Returns
573
+ -------
574
+ Tag
575
+ The tag with the specified name.
576
+ """
577
+ try:
578
+ return next(filter(lambda t: t.name == name, self.list_tags()))
579
+ except StopIteration:
580
+ raise ValueError(f"Tag '{name}' not found in geometry {self.id}")
581
+
522
582
  def list_entities(self) -> tuple[list[Surface], list[Volume]]:
523
583
  """
524
584
  List all the entities in the geometry.
@@ -31,6 +31,10 @@ class GeometryVersion(ProtoWrapperBase):
31
31
  """
32
32
  return timestamp_to_datetime(self._proto.create_time)
33
33
 
34
+ @property
35
+ def url(self) -> str:
36
+ return f"{self.geometry().url}/version/{self.id}"
37
+
34
38
  def geometry(self) -> Geometry:
35
39
  """
36
40
  Get the parent geometry.
luminarycloud/mesh.py CHANGED
@@ -38,6 +38,10 @@ class Mesh(ProtoWrapperBase):
38
38
  def create_time(self) -> datetime:
39
39
  return timestamp_to_datetime(self._proto.create_time)
40
40
 
41
+ @property
42
+ def url(self) -> str:
43
+ return f"{self.project().url}/mesh/{self.id}"
44
+
41
45
  def project(self) -> "Project":
42
46
  """
43
47
  Get the project this mesh belongs to.
@@ -16,11 +16,10 @@ from ..params.geometry import (
16
16
  )
17
17
  from ..types import Vector3
18
18
  from .sizing_strategy import MaxCount, Minimal, MinimalCount, SizingStrategy, TargetCount
19
- from ..pipeline_util.dictable import PipelineDictable
20
19
 
21
20
 
22
21
  @dataclass(kw_only=True)
23
- class VolumeMeshingParams(PipelineDictable):
22
+ class VolumeMeshingParams:
24
23
  """Volume meshing parameters."""
25
24
 
26
25
  volumes: list[Volume]
@@ -39,7 +38,7 @@ class VolumeMeshingParams(PipelineDictable):
39
38
 
40
39
 
41
40
  @dataclass(kw_only=True)
42
- class ModelMeshingParams(PipelineDictable):
41
+ class ModelMeshingParams:
43
42
  """Model meshing parameters."""
44
43
 
45
44
  surfaces: Sequence[Surface | str]
@@ -62,7 +61,7 @@ class ModelMeshingParams(PipelineDictable):
62
61
 
63
62
 
64
63
  @dataclass(kw_only=True)
65
- class BoundaryLayerParams(PipelineDictable):
64
+ class BoundaryLayerParams:
66
65
  """Boundary layer meshing parameters."""
67
66
 
68
67
  surfaces: Sequence[Surface | str]
@@ -88,7 +87,7 @@ class BoundaryLayerParams(PipelineDictable):
88
87
 
89
88
 
90
89
  @dataclass(kw_only=True)
91
- class RefinementRegion(PipelineDictable):
90
+ class RefinementRegion:
92
91
  """Refinement region parameters."""
93
92
 
94
93
  name: str
@@ -108,15 +107,15 @@ class RefinementRegion(PipelineDictable):
108
107
  proto.name = self.name
109
108
  proto.h_limit = self.h_limit
110
109
  if isinstance(self.shape, Sphere):
111
- proto.sphere.center.CopyFrom(self.shape.center._to_base_proto())
112
- proto.sphere.radius = self.shape.radius
110
+ proto.sphere.center_float.CopyFrom(self.shape.center._to_base_proto())
111
+ proto.sphere.radius_float = self.shape.radius
113
112
  elif isinstance(self.shape, SphereShell):
114
113
  proto.sphere_shell.center.CopyFrom(self.shape.center._to_base_proto())
115
114
  proto.sphere_shell.radius = self.shape.radius
116
115
  proto.sphere_shell.radius_inner = self.shape.radius_inner
117
116
  elif isinstance(self.shape, Cube):
118
- proto.cube.min.CopyFrom(self.shape.min._to_base_proto())
119
- proto.cube.max.CopyFrom(self.shape.max._to_base_proto())
117
+ proto.cube.min_float.CopyFrom(self.shape.min._to_base_proto())
118
+ proto.cube.max_float.CopyFrom(self.shape.max._to_base_proto())
120
119
  elif isinstance(self.shape, OrientedCube):
121
120
  proto.oriented_cube.min.CopyFrom(self.shape.min._to_base_proto())
122
121
  proto.oriented_cube.max.CopyFrom(self.shape.max._to_base_proto())
@@ -124,9 +123,9 @@ class RefinementRegion(PipelineDictable):
124
123
  proto.oriented_cube.x_axis.CopyFrom(self.shape.x_axis._to_base_proto())
125
124
  proto.oriented_cube.y_axis.CopyFrom(self.shape.y_axis._to_base_proto())
126
125
  elif isinstance(self.shape, Cylinder):
127
- proto.cylinder.start.CopyFrom(self.shape.start._to_base_proto())
128
- proto.cylinder.end.CopyFrom(self.shape.end._to_base_proto())
129
- proto.cylinder.radius = self.shape.radius
126
+ proto.cylinder.start_float.CopyFrom(self.shape.start._to_base_proto())
127
+ proto.cylinder.end_float.CopyFrom(self.shape.end._to_base_proto())
128
+ proto.cylinder.radius_float = self.shape.radius
130
129
  elif isinstance(self.shape, AnnularCylinder):
131
130
  proto.annular_cylinder.start.CopyFrom(self.shape.start._to_base_proto())
132
131
  proto.annular_cylinder.end.CopyFrom(self.shape.end._to_base_proto())
@@ -138,12 +137,12 @@ class RefinementRegion(PipelineDictable):
138
137
 
139
138
 
140
139
  @dataclass(kw_only=True)
141
- class MeshGenerationParams(PipelineDictable):
140
+ class MeshGenerationParams:
142
141
  """Mesh generation parameters."""
143
142
 
144
143
  geometry_id: str
145
144
  "The ID of the geometry to generate a mesh for"
146
- sizing_strategy: SizingStrategy = field(default_factory=MaxCount)
145
+ sizing_strategy: SizingStrategy = field(default_factory=Minimal)
147
146
  "The sizing strategy to use"
148
147
 
149
148
  # Defaults copied from ts/frontend/src/lib/paramDefaults/meshGenerationState.ts
@@ -1,11 +1,10 @@
1
1
  from dataclasses import dataclass
2
2
 
3
3
  from luminarycloud._helpers.warnings.deprecated import deprecated
4
- from ...pipeline_util.dictable import PipelineDictable
5
4
 
6
5
 
7
6
  @dataclass
8
- class SizingStrategy(PipelineDictable):
7
+ class SizingStrategy:
9
8
  """Sizing strategy parameters."""
10
9
 
11
10
  pass
@@ -17,6 +17,7 @@ def _download_processed_solution_physics_ai( # noqa: F841
17
17
  process_volume: bool = False,
18
18
  single_precision: bool = True,
19
19
  internal_options: Optional[Dict[str, str]] = None,
20
+ export_surface_groups: Optional[Dict[str, List[str]]] = None,
20
21
  ) -> tarfile.TarFile:
21
22
  """
22
23
  Download solution data with physics AI processing applied.
@@ -37,6 +38,8 @@ def _download_processed_solution_physics_ai( # noqa: F841
37
38
  If None, all available volume fields are included.
38
39
  process_volume: Whether to process volume data
39
40
  single_precision: Whether to use single precision for floating point fields
41
+ export_surface_groups: Dictionary mapping group names to lists of surface names.
42
+ Each group will be exported as an individual STL file.
40
43
 
41
44
  Raises:
42
45
  ValueError: If invalid field names are provided
@@ -46,6 +49,7 @@ def _download_processed_solution_physics_ai( # noqa: F841
46
49
  get_default_client(),
47
50
  solution_id,
48
51
  exclude_surfaces=exclude_surfaces,
52
+ export_surface_groups=export_surface_groups,
49
53
  fill_holes=fill_holes,
50
54
  surface_fields_to_keep=surface_fields_to_keep,
51
55
  volume_fields_to_keep=volume_fields_to_keep,
@@ -7,6 +7,7 @@ from time import time, sleep
7
7
  import logging
8
8
 
9
9
  from .arguments import PipelineArgValueType
10
+ from .core import Stage
10
11
  from ..pipelines import Pipeline, PipelineArgs
11
12
  from .._client import get_default_client
12
13
 
@@ -69,6 +70,25 @@ class PipelineRecord:
69
70
  res = get_default_client().http.get(f"/rest/v0/pipelines/{self.id}/pipeline_jobs")
70
71
  return [PipelineJobRecord.from_json(p) for p in res["data"]]
71
72
 
73
+ def delete(self) -> None:
74
+ """
75
+ Delete this pipeline.
76
+
77
+ This will permanently delete the pipeline and all associated pipeline jobs.
78
+ This operation cannot be undone.
79
+
80
+ Raises
81
+ ------
82
+ HTTPException
83
+ If the pipeline does not exist or if you do not have permission to delete it.
84
+
85
+ Examples
86
+ --------
87
+ >>> pipeline = pipelines.get_pipeline("pipeline-123")
88
+ >>> pipeline.delete()
89
+ """
90
+ get_default_client().http.delete(f"/rest/v0/pipelines/{self.id}")
91
+
72
92
 
73
93
  @dataclass
74
94
  class PipelineJobRecord:
@@ -80,7 +100,7 @@ class PipelineJobRecord:
80
100
  pipeline_id: str
81
101
  name: str
82
102
  description: str | None
83
- status: Literal["pending", "running", "completed", "failed"]
103
+ status: Literal["pending", "running", "completed", "failed", "cancelled"]
84
104
  created_at: datetime
85
105
  updated_at: datetime
86
106
  started_at: datetime | None
@@ -156,18 +176,37 @@ class PipelineJobRecord:
156
176
  res = get_default_client().http.get(f"/rest/v0/pipeline_jobs/{self.id}/artifacts")
157
177
  return res["data"]
158
178
 
179
+ def delete(self) -> None:
180
+ """
181
+ Delete this pipeline job.
182
+
183
+ This will permanently delete the pipeline job and all associated runs and tasks.
184
+ This operation cannot be undone.
185
+
186
+ Raises
187
+ ------
188
+ HTTPException
189
+ If the pipeline job does not exist or if you do not have permission to delete it.
190
+
191
+ Examples
192
+ --------
193
+ >>> pipeline_job = pipelines.get_pipeline_job("pipelinejob-123")
194
+ >>> pipeline_job.delete()
195
+ """
196
+ get_default_client().http.delete(f"/rest/v0/pipeline_jobs/{self.id}")
197
+
159
198
  def wait(
160
199
  self,
161
200
  *,
162
201
  interval_seconds: float = 5,
163
202
  timeout_seconds: float = float("inf"),
164
203
  print_logs: bool = False,
165
- ) -> Literal["completed", "failed"]:
204
+ ) -> Literal["completed", "failed", "cancelled"]:
166
205
  """
167
- Wait for the pipeline job to complete or fail.
206
+ Wait for the pipeline job to complete, fail, or be cancelled.
168
207
 
169
208
  This method polls the pipeline job status at regular intervals until it reaches
170
- a terminal state (completed or failed).
209
+ a terminal state (completed, failed, or cancelled).
171
210
 
172
211
  Parameters
173
212
  ----------
@@ -180,7 +219,7 @@ class PipelineJobRecord:
180
219
 
181
220
  Returns
182
221
  -------
183
- Literal["completed", "failed"]
222
+ Literal["completed", "failed", "cancelled"]
184
223
  The final status of the pipeline job.
185
224
 
186
225
  Raises
@@ -216,6 +255,9 @@ class PipelineJobRecord:
216
255
  elif updated_job.status == "failed":
217
256
  logger.warning(f"Pipeline job {self.id} failed")
218
257
  return "failed"
258
+ elif updated_job.status == "cancelled":
259
+ logger.info(f"Pipeline job {self.id} was cancelled")
260
+ return "cancelled"
219
261
 
220
262
  # Check timeout
221
263
  if time() >= deadline:
@@ -233,13 +275,53 @@ class PipelineJobRecord:
233
275
  self.started_at = updated_job.started_at
234
276
  self.completed_at = updated_job.completed_at
235
277
 
278
+ def get_concurrency_limits(self) -> dict[str, int]:
279
+ """
280
+ Returns the concurrency limits for this pipeline job.
281
+
282
+ Returns
283
+ -------
284
+ dict[str, int]
285
+ A dictionary mapping stage IDs to their concurrency limits.
286
+ """
287
+ res = get_default_client().http.get(f"/rest/v0/pipeline_jobs/{self.id}/concurrency_limits")
288
+ return {k: v["limit"] for k, v in res["data"].items()}
289
+
290
+ def set_concurrency_limits(self, limits: dict[str, int]) -> None:
291
+ """
292
+ Sets the concurrency limits for this pipeline job.
293
+
294
+ Parameters
295
+ ----------
296
+ limits : dict[str, int]
297
+ A dictionary mapping stage IDs to their concurrency limits.
298
+ """
299
+ body = {k: {"limit": v} for k, v in limits.items()}
300
+ get_default_client().http.put(f"/rest/v0/pipeline_jobs/{self.id}/concurrency_limits", body)
301
+
302
+ def cancel(self) -> None:
303
+ """Cancel this running pipeline job.
304
+
305
+ This will request cancellation of the underlying Prefect flow run. The
306
+ job should eventually transition to a cancelled terminal state once
307
+ the backend processes the cancellation.
308
+
309
+ Raises
310
+ ------
311
+ HTTPError
312
+ If the pipeline job cannot be cancelled (e.g., not found, not
313
+ running, or lacks the necessary Prefect flow run ID).
314
+ """
315
+ get_default_client().http.post(f"/rest/v0/pipeline_jobs/{self.id}/cancel", {})
316
+ logger.info(f"Cancelled pipeline job {self.id}")
317
+
236
318
 
237
319
  @dataclass
238
320
  class PipelineJobRunRecord:
239
321
  pipeline_job_id: str
240
322
  idx: int
241
323
  arguments: list[PipelineArgValueType]
242
- status: Literal["pending", "running", "completed", "failed"]
324
+ status: Literal["pending", "running", "completed", "failed", "cancelled"]
243
325
 
244
326
  @classmethod
245
327
  def from_json(cls, json: dict) -> "PipelineJobRunRecord":
@@ -347,7 +429,11 @@ def get_pipeline(id: str) -> PipelineRecord:
347
429
 
348
430
 
349
431
  def create_pipeline_job(
350
- pipeline_id: str, args: PipelineArgs, name: str, description: str | None = None
432
+ pipeline_id: str,
433
+ args: PipelineArgs,
434
+ name: str,
435
+ description: str | None = None,
436
+ concurrency_limits: dict[str, int] | None = None,
351
437
  ) -> PipelineJobRecord:
352
438
  """
353
439
  Create a new pipeline job.
@@ -362,6 +448,8 @@ def create_pipeline_job(
362
448
  Name of the pipeline job.
363
449
  description : str, optional
364
450
  Description of the pipeline job.
451
+ concurrency_limits : dict[str, int], optional
452
+ A dictionary mapping stage IDs to their concurrency limits.
365
453
  """
366
454
 
367
455
  arg_rows = [row.row_values for row in args.rows]
@@ -373,7 +461,10 @@ def create_pipeline_job(
373
461
  }
374
462
 
375
463
  res = get_default_client().http.post(f"/rest/v0/pipelines/{pipeline_id}/pipeline_jobs", body)
376
- return PipelineJobRecord.from_json(res["data"])
464
+ pjr = PipelineJobRecord.from_json(res["data"])
465
+ if concurrency_limits is not None:
466
+ pjr.set_concurrency_limits(concurrency_limits)
467
+ return pjr
377
468
 
378
469
 
379
470
  def get_pipeline_job(id: str) -> PipelineJobRecord:
@@ -249,9 +249,16 @@ class Stage(Generic[TOutputs], ABC):
249
249
  for name, value in self._params.items():
250
250
  if hasattr(value, "_to_pipeline_dict"):
251
251
  d[name], downstream_params = value._to_pipeline_dict()
252
+ for param in downstream_params:
253
+ if not isinstance(param, PipelineParameter):
254
+ raise ValueError(
255
+ f"Expected `_to_pipeline_dict()` to only return PipelineParameters, but got a {type(param)}: {param}"
256
+ )
252
257
  pipeline_params.update(downstream_params)
253
258
  else:
254
259
  d[name] = value
260
+ # Strip None values. We treat absence of a param value in the YAML the same as a present null value.
261
+ d = {k: v for k, v in d.items() if v is not None}
255
262
  return d, pipeline_params
256
263
 
257
264
  def __str__(self) -> str:
@@ -285,7 +292,7 @@ class Pipeline:
285
292
  def pipeline_params(self) -> set[PipelineParameter]:
286
293
  return self._stages_dict_and_params()[1]
287
294
 
288
- def _get_stage_id(self, stage: Stage) -> str:
295
+ def get_stage_id(self, stage: Stage) -> str:
289
296
  return self._stage_ids[stage]
290
297
 
291
298
  def _stages_dict_and_params(self) -> tuple[dict, set[PipelineParameter]]:
@@ -361,7 +368,10 @@ class Pipeline:
361
368
  for stage_id in d["tasks"]:
362
369
  _parse_stage(d, stage_id, parsed_stages)
363
370
 
364
- return cls(list(parsed_stages.values()))
371
+ pipe = cls(list(parsed_stages.values()))
372
+ # Preserve the stage IDs from the YAML definition by overwriting the auto-assigned ones
373
+ pipe._stage_ids = {stage: stage_id for stage_id, stage in parsed_stages.items()}
374
+ return pipe
365
375
 
366
376
 
367
377
  def _recursive_replace_pipeline_params(d: Any, parsed_params: dict) -> Any: