luminarycloud 0.22.0__py3-none-any.whl → 0.22.2__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 (86) hide show
  1. luminarycloud/_client/authentication_plugin.py +49 -0
  2. luminarycloud/_client/client.py +38 -11
  3. luminarycloud/_client/http_client.py +1 -1
  4. luminarycloud/_client/retry_interceptor.py +64 -2
  5. luminarycloud/_helpers/__init__.py +9 -0
  6. luminarycloud/_helpers/_inference_jobs.py +227 -0
  7. luminarycloud/_helpers/_parse_iso_datetime.py +54 -0
  8. luminarycloud/_helpers/download.py +11 -0
  9. luminarycloud/_helpers/proto_decorator.py +38 -7
  10. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +152 -132
  11. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +66 -8
  12. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.py +34 -0
  13. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.pyi +12 -0
  14. luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.py +142 -39
  15. luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.pyi +300 -3
  16. luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2_grpc.py +34 -0
  17. luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2_grpc.pyi +12 -0
  18. luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2.py +255 -0
  19. luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2.pyi +466 -0
  20. luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2_grpc.py +242 -0
  21. luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2_grpc.pyi +95 -0
  22. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.py +29 -7
  23. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.pyi +39 -0
  24. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.py +36 -0
  25. luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.pyi +18 -0
  26. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.py +88 -65
  27. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.pyi +42 -0
  28. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.py +34 -0
  29. luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.pyi +12 -0
  30. luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.py +163 -153
  31. luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.pyi +37 -3
  32. luminarycloud/_proto/base/base_pb2.py +7 -6
  33. luminarycloud/_proto/base/base_pb2.pyi +4 -0
  34. luminarycloud/_proto/client/simulation_pb2.py +358 -339
  35. luminarycloud/_proto/client/simulation_pb2.pyi +89 -3
  36. luminarycloud/_proto/physicsaiinferenceservice/physicsaiinferenceservice_pb2.py +35 -0
  37. luminarycloud/_proto/physicsaiinferenceservice/physicsaiinferenceservice_pb2.pyi +7 -0
  38. luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2.py +6 -3
  39. luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.py +68 -0
  40. luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.pyi +24 -0
  41. luminarycloud/_wrapper.py +53 -7
  42. luminarycloud/enum/vis_enums.py +6 -0
  43. luminarycloud/feature_modification.py +25 -32
  44. luminarycloud/geometry.py +10 -6
  45. luminarycloud/geometry_version.py +4 -0
  46. luminarycloud/mesh.py +4 -0
  47. luminarycloud/meshing/mesh_generation_params.py +5 -6
  48. luminarycloud/meshing/sizing_strategy/sizing_strategies.py +1 -2
  49. luminarycloud/outputs/__init__.py +2 -0
  50. luminarycloud/outputs/output_definitions.py +3 -3
  51. luminarycloud/outputs/stopping_conditions.py +94 -0
  52. luminarycloud/params/enum/_enum_wrappers.py +16 -0
  53. luminarycloud/params/geometry/shapes.py +33 -33
  54. luminarycloud/params/simulation/adaptive_mesh_refinement/__init__.py +1 -0
  55. luminarycloud/params/simulation/adaptive_mesh_refinement/active_region_.py +83 -0
  56. luminarycloud/params/simulation/adaptive_mesh_refinement/boundary_layer_profile_.py +1 -1
  57. luminarycloud/params/simulation/adaptive_mesh_refinement_.py +8 -1
  58. luminarycloud/physics_ai/__init__.py +7 -0
  59. luminarycloud/physics_ai/inference.py +166 -199
  60. luminarycloud/physics_ai/models.py +22 -0
  61. luminarycloud/physics_ai/solution.py +4 -0
  62. luminarycloud/pipelines/api.py +143 -16
  63. luminarycloud/pipelines/core.py +1 -1
  64. luminarycloud/pipelines/stages.py +22 -9
  65. luminarycloud/project.py +61 -8
  66. luminarycloud/simulation.py +25 -0
  67. luminarycloud/types/__init__.py +2 -0
  68. luminarycloud/types/ids.py +2 -0
  69. luminarycloud/types/vector3.py +1 -2
  70. luminarycloud/vis/__init__.py +1 -0
  71. luminarycloud/vis/data_extraction.py +7 -7
  72. luminarycloud/vis/filters.py +97 -0
  73. luminarycloud/vis/interactive_report.py +163 -7
  74. luminarycloud/vis/report.py +113 -1
  75. luminarycloud/vis/visualization.py +3 -0
  76. luminarycloud/volume_selection.py +16 -8
  77. luminarycloud/workflow_utils.py +149 -0
  78. {luminarycloud-0.22.0.dist-info → luminarycloud-0.22.2.dist-info}/METADATA +1 -1
  79. {luminarycloud-0.22.0.dist-info → luminarycloud-0.22.2.dist-info}/RECORD +80 -76
  80. {luminarycloud-0.22.0.dist-info → luminarycloud-0.22.2.dist-info}/WHEEL +1 -1
  81. luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.py +0 -61
  82. luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.pyi +0 -85
  83. luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2_grpc.py +0 -67
  84. luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2_grpc.pyi +0 -26
  85. luminarycloud/_proto/inferenceservice/inferenceservice_pb2.py +0 -69
  86. luminarycloud/pipeline_util/dictable.py +0 -27
@@ -9,6 +9,7 @@ import google.protobuf.internal.containers
9
9
  import google.protobuf.internal.enum_type_wrapper
10
10
  import google.protobuf.message
11
11
  import luminarycloud._proto.base.base_pb2
12
+ import luminarycloud._proto.cad.shape_pb2
12
13
  import luminarycloud._proto.client.entity_pb2
13
14
  import luminarycloud._proto.entitygroup.entitygroup_pb2
14
15
  import luminarycloud._proto.output.output_pb2
@@ -740,6 +741,14 @@ class _PtTableFluidEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._E
740
741
  """R410A."""
741
742
  REAL_GAS_R507A: _PtTableFluid.ValueType # 56561
742
743
  """R507A."""
744
+ REAL_GAS_R513A: _PtTableFluid.ValueType # 13621
745
+ """R513A."""
746
+ REAL_GAS_R513B: _PtTableFluid.ValueType # 5656
747
+ """R513B."""
748
+ REAL_GAS_R515A: _PtTableFluid.ValueType # 55001
749
+ """R515A."""
750
+ REAL_GAS_R515B: _PtTableFluid.ValueType # 5076
751
+ """R515B."""
743
752
  REAL_GAS_RC318: _PtTableFluid.ValueType # 66529
744
753
  """RC318."""
745
754
  REAL_GAS_SES36: _PtTableFluid.ValueType # 14408
@@ -990,6 +999,14 @@ REAL_GAS_R410A: PtTableFluid.ValueType # 59981
990
999
  """R410A."""
991
1000
  REAL_GAS_R507A: PtTableFluid.ValueType # 56561
992
1001
  """R507A."""
1002
+ REAL_GAS_R513A: PtTableFluid.ValueType # 13621
1003
+ """R513A."""
1004
+ REAL_GAS_R513B: PtTableFluid.ValueType # 5656
1005
+ """R513B."""
1006
+ REAL_GAS_R515A: PtTableFluid.ValueType # 55001
1007
+ """R515A."""
1008
+ REAL_GAS_R515B: PtTableFluid.ValueType # 5076
1009
+ """R515B."""
993
1010
  REAL_GAS_RC318: PtTableFluid.ValueType # 66529
994
1011
  """RC318."""
995
1012
  REAL_GAS_SES36: PtTableFluid.ValueType # 14408
@@ -4469,7 +4486,7 @@ class ReferenceValues(google.protobuf.message.Message):
4469
4486
  global___ReferenceValues = ReferenceValues
4470
4487
 
4471
4488
  class BoundaryLayerProfile(google.protobuf.message.Message):
4472
- """TODO: no help"""
4489
+ """Boundary layer meshing parameters to apply to adapted meshes."""
4473
4490
 
4474
4491
  DESCRIPTOR: google.protobuf.descriptor.Descriptor
4475
4492
 
@@ -4502,6 +4519,67 @@ class BoundaryLayerProfile(google.protobuf.message.Message):
4502
4519
 
4503
4520
  global___BoundaryLayerProfile = BoundaryLayerProfile
4504
4521
 
4522
+ class ActiveRegion(google.protobuf.message.Message):
4523
+ """Region(s) within which the mesh is adapted at full resolution. Outside
4524
+ of these regions the mesh is coarsened with increasing distance from the
4525
+ region.
4526
+ """
4527
+
4528
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
4529
+
4530
+ ERROR_WEIGHT_FIELD_NUMBER: builtins.int
4531
+ MAX_DISTANCE_FIELD_NUMBER: builtins.int
4532
+ CUBE_FIELD_NUMBER: builtins.int
4533
+ ORIENTED_CUBE_FIELD_NUMBER: builtins.int
4534
+ CYLINDER_FIELD_NUMBER: builtins.int
4535
+ ANNULAR_CYLINDER_FIELD_NUMBER: builtins.int
4536
+ SPHERE_FIELD_NUMBER: builtins.int
4537
+ SPHERE_SHELL_FIELD_NUMBER: builtins.int
4538
+ @property
4539
+ def error_weight(self) -> luminarycloud._proto.base.base_pb2.AdFloatType:
4540
+ """Error weighting from the active region at Max Distance. Local error
4541
+ weighting is 1.0 inside the region and decreases to Error Weight at the Max
4542
+ Distance.
4543
+ """
4544
+ @property
4545
+ def max_distance(self) -> luminarycloud._proto.base.base_pb2.AdFloatType:
4546
+ """Distance from the active regions at which to apply the full error factor."""
4547
+ @property
4548
+ def cube(self) -> luminarycloud._proto.cad.shape_pb2.Cube:
4549
+ """Coordinate-aligned box."""
4550
+ @property
4551
+ def oriented_cube(self) -> luminarycloud._proto.cad.shape_pb2.OrientedCube:
4552
+ """Box oriented along arbitrary axes."""
4553
+ @property
4554
+ def cylinder(self) -> luminarycloud._proto.cad.shape_pb2.Cylinder:
4555
+ """Cylinder shape."""
4556
+ @property
4557
+ def annular_cylinder(self) -> luminarycloud._proto.cad.shape_pb2.AnnularCylinder:
4558
+ """Cylinder with a hole along the axis."""
4559
+ @property
4560
+ def sphere(self) -> luminarycloud._proto.cad.shape_pb2.Sphere:
4561
+ """Sphere shape."""
4562
+ @property
4563
+ def sphere_shell(self) -> luminarycloud._proto.cad.shape_pb2.SphereShell:
4564
+ """Spherical shell shape."""
4565
+ def __init__(
4566
+ self,
4567
+ *,
4568
+ error_weight: luminarycloud._proto.base.base_pb2.AdFloatType | None = ...,
4569
+ max_distance: luminarycloud._proto.base.base_pb2.AdFloatType | None = ...,
4570
+ cube: luminarycloud._proto.cad.shape_pb2.Cube | None = ...,
4571
+ oriented_cube: luminarycloud._proto.cad.shape_pb2.OrientedCube | None = ...,
4572
+ cylinder: luminarycloud._proto.cad.shape_pb2.Cylinder | None = ...,
4573
+ annular_cylinder: luminarycloud._proto.cad.shape_pb2.AnnularCylinder | None = ...,
4574
+ sphere: luminarycloud._proto.cad.shape_pb2.Sphere | None = ...,
4575
+ sphere_shell: luminarycloud._proto.cad.shape_pb2.SphereShell | None = ...,
4576
+ ) -> None: ...
4577
+ def HasField(self, field_name: typing_extensions.Literal["annular_cylinder", b"annular_cylinder", "cube", b"cube", "cylinder", b"cylinder", "error_weight", b"error_weight", "max_distance", b"max_distance", "oriented_cube", b"oriented_cube", "shape", b"shape", "sphere", b"sphere", "sphere_shell", b"sphere_shell"]) -> builtins.bool: ...
4578
+ def ClearField(self, field_name: typing_extensions.Literal["annular_cylinder", b"annular_cylinder", "cube", b"cube", "cylinder", b"cylinder", "error_weight", b"error_weight", "max_distance", b"max_distance", "oriented_cube", b"oriented_cube", "shape", b"shape", "sphere", b"sphere", "sphere_shell", b"sphere_shell"]) -> None: ...
4579
+ def WhichOneof(self, oneof_group: typing_extensions.Literal["shape", b"shape"]) -> typing_extensions.Literal["cube", "oriented_cube", "cylinder", "annular_cylinder", "sphere", "sphere_shell"] | None: ...
4580
+
4581
+ global___ActiveRegion = ActiveRegion
4582
+
4505
4583
  class AdaptiveMeshRefinement(google.protobuf.message.Message):
4506
4584
  """Adaptive Mesh Refinement"""
4507
4585
 
@@ -4517,6 +4595,7 @@ class AdaptiveMeshRefinement(google.protobuf.message.Message):
4517
4595
  ALL_TET_FIELD_NUMBER: builtins.int
4518
4596
  USER_SCALING_FIELD_NUMBER: builtins.int
4519
4597
  BOUNDARY_LAYER_PROFILE_FIELD_NUMBER: builtins.int
4598
+ ACTIVE_REGION_FIELD_NUMBER: builtins.int
4520
4599
  @property
4521
4600
  def refinement_iterations(self) -> luminarycloud._proto.base.base_pb2.Int:
4522
4601
  """Number of refinement iterations to perform."""
@@ -4546,7 +4625,13 @@ class AdaptiveMeshRefinement(google.protobuf.message.Message):
4546
4625
  """Scale factor between the geometry and the mesh."""
4547
4626
  @property
4548
4627
  def boundary_layer_profile(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BoundaryLayerProfile]:
4549
- """TODO: no help"""
4628
+ """Boundary layer meshing parameters to apply to adapted meshes."""
4629
+ @property
4630
+ def active_region(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ActiveRegion]:
4631
+ """Region(s) within which the mesh is adapted at full resolution. Outside
4632
+ of these regions the mesh is coarsened with increasing distance from the
4633
+ region.
4634
+ """
4550
4635
  def __init__(
4551
4636
  self,
4552
4637
  *,
@@ -4560,9 +4645,10 @@ class AdaptiveMeshRefinement(google.protobuf.message.Message):
4560
4645
  all_tet: global___AllTet.ValueType = ...,
4561
4646
  user_scaling: luminarycloud._proto.base.base_pb2.AdFloatType | None = ...,
4562
4647
  boundary_layer_profile: collections.abc.Iterable[global___BoundaryLayerProfile] | None = ...,
4648
+ active_region: collections.abc.Iterable[global___ActiveRegion] | None = ...,
4563
4649
  ) -> None: ...
4564
4650
  def HasField(self, field_name: typing_extensions.Literal["final_target_complexity", b"final_target_complexity", "initial_target_complexity", b"initial_target_complexity", "max_refinement_interval", b"max_refinement_interval", "refinement_dispatch_interval", b"refinement_dispatch_interval", "refinement_iterations", b"refinement_iterations", "target_cv_millions", b"target_cv_millions", "user_scaling", b"user_scaling"]) -> builtins.bool: ...
4565
- def ClearField(self, field_name: typing_extensions.Literal["all_tet", b"all_tet", "boundary_layer_profile", b"boundary_layer_profile", "final_target_complexity", b"final_target_complexity", "initial_target_complexity", b"initial_target_complexity", "max_refinement_interval", b"max_refinement_interval", "meshing_method", b"meshing_method", "refinement_dispatch_interval", b"refinement_dispatch_interval", "refinement_iterations", b"refinement_iterations", "target_cv_millions", b"target_cv_millions", "user_scaling", b"user_scaling"]) -> None: ...
4651
+ def ClearField(self, field_name: typing_extensions.Literal["active_region", b"active_region", "all_tet", b"all_tet", "boundary_layer_profile", b"boundary_layer_profile", "final_target_complexity", b"final_target_complexity", "initial_target_complexity", b"initial_target_complexity", "max_refinement_interval", b"max_refinement_interval", "meshing_method", b"meshing_method", "refinement_dispatch_interval", b"refinement_dispatch_interval", "refinement_iterations", b"refinement_iterations", "target_cv_millions", b"target_cv_millions", "user_scaling", b"user_scaling"]) -> None: ...
4566
4652
 
4567
4653
  global___AdaptiveMeshRefinement = AdaptiveMeshRefinement
4568
4654
 
@@ -0,0 +1,35 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: proto/physicsaiinferenceservice/physicsaiinferenceservice.proto
4
+ """Generated protocol buffer code."""
5
+ from google.protobuf import descriptor as _descriptor
6
+ from google.protobuf import descriptor_pool as _descriptor_pool
7
+ from google.protobuf import message as _message
8
+ from google.protobuf import reflection as _reflection
9
+ from google.protobuf import symbol_database as _symbol_database
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
15
+ from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
16
+ from luminarycloud._proto.api.v0.luminarycloud.physicsaiinference import physicsaiinference_pb2 as proto_dot_api_dot_v0_dot_luminarycloud_dot_physicsaiinference_dot_physicsaiinference__pb2
17
+ from luminarycloud._proto.ratelimit import ratelimit_pb2 as proto_dot_ratelimit_dot_ratelimit__pb2
18
+
19
+
20
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n?proto/physicsaiinferenceservice/physicsaiinferenceservice.proto\x12(luminary.proto.physicsaiinferenceservice\x1a\x1bgoogle/protobuf/empty.proto\x1a\x46proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference.proto\x1a\x1fproto/ratelimit/ratelimit.proto2\xb1\t\n\x19PhysicsAiInferenceService\x12\xd9\x01\n\x19\x43reateInferenceServiceJob\x12X.luminary.proto.api.v0.luminarycloud.physicsaiinference.CreateInferenceServiceJobRequest\x1aV.luminary.proto.api.v0.luminarycloud.physicsaiinference.GetInferenceServiceJobResponse\"\n\x8a\xb5\x18\x06\x08\n\x12\x02\x08\x01\x12\xde\x01\n\x1e\x43reateInferenceServiceJobAsync\x12X.luminary.proto.api.v0.luminarycloud.physicsaiinference.CreateInferenceServiceJobRequest\x1aV.luminary.proto.api.v0.luminarycloud.physicsaiinference.GetInferenceServiceJobResponse\"\n\x8a\xb5\x18\x06\x08\n\x12\x02\x08\x01\x12\xbb\x01\n\x12GetInferenceFields\x12Q.luminary.proto.api.v0.luminarycloud.physicsaiinference.GetInferenceFieldsRequest\x1aR.luminary.proto.api.v0.luminarycloud.physicsaiinference.GetInferenceFieldsResponse\x12\xc7\x01\n\x16GetInferenceServiceJob\x12U.luminary.proto.api.v0.luminarycloud.physicsaiinference.GetInferenceServiceJobRequest\x1aV.luminary.proto.api.v0.luminarycloud.physicsaiinference.GetInferenceServiceJobResponse\x12\xcd\x01\n\x18ListInferenceServiceJobs\x12W.luminary.proto.api.v0.luminarycloud.physicsaiinference.ListInferenceServiceJobsRequest\x1aX.luminary.proto.api.v0.luminarycloud.physicsaiinference.ListInferenceServiceJobsResponse\x12\x7f\n\x12\x44\x65leteInferenceJob\x12Q.luminary.proto.api.v0.luminarycloud.physicsaiinference.DeleteInferenceJobRequest\x1a\x16.google.protobuf.EmptyB8Z6luminarycloud.com/core/proto/physicsaiinferenceserviceb\x06proto3')
21
+
22
+
23
+
24
+ _PHYSICSAIINFERENCESERVICE = DESCRIPTOR.services_by_name['PhysicsAiInferenceService']
25
+ if _descriptor._USE_C_DESCRIPTORS == False:
26
+
27
+ DESCRIPTOR._options = None
28
+ DESCRIPTOR._serialized_options = b'Z6luminarycloud.com/core/proto/physicsaiinferenceservice'
29
+ _PHYSICSAIINFERENCESERVICE.methods_by_name['CreateInferenceServiceJob']._options = None
30
+ _PHYSICSAIINFERENCESERVICE.methods_by_name['CreateInferenceServiceJob']._serialized_options = b'\212\265\030\006\010\n\022\002\010\001'
31
+ _PHYSICSAIINFERENCESERVICE.methods_by_name['CreateInferenceServiceJobAsync']._options = None
32
+ _PHYSICSAIINFERENCESERVICE.methods_by_name['CreateInferenceServiceJobAsync']._serialized_options = b'\212\265\030\006\010\n\022\002\010\001'
33
+ _PHYSICSAIINFERENCESERVICE._serialized_start=244
34
+ _PHYSICSAIINFERENCESERVICE._serialized_end=1445
35
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,7 @@
1
+ """
2
+ @generated by mypy-protobuf. Do not edit manually!
3
+ isort:skip_file
4
+ """
5
+ import google.protobuf.descriptor
6
+
7
+ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@@ -13,9 +13,10 @@ _sym_db = _symbol_database.Default()
13
13
 
14
14
 
15
15
  from luminarycloud._proto.api.v0.luminarycloud.physics_ai import physics_ai_pb2 as proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2
16
+ from luminarycloud._proto.ratelimit import ratelimit_pb2 as proto_dot_ratelimit_dot_ratelimit__pb2
16
17
 
17
18
 
18
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n=proto/physicsaitrainingservice/physicsaitrainingservice.proto\x12\'luminary.proto.physicsaitrainingservice\x1a\x36proto/api/v0/luminarycloud/physics_ai/physics_ai.proto2\xc7\x01\n\x18PhysicsAiTrainingService\x12\xaa\x01\n\x11SubmitTrainingJob\x12H.luminary.proto.api.v0.luminarycloud.physics_ai.SubmitTrainingJobRequest\x1aI.luminary.proto.api.v0.luminarycloud.physics_ai.SubmitTrainingJobResponse\"\x00\x42\x37Z5luminarycloud.com/core/proto/physicsaitrainingserviceb\x06proto3')
19
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n=proto/physicsaitrainingservice/physicsaitrainingservice.proto\x12\'luminary.proto.physicsaitrainingservice\x1a\x36proto/api/v0/luminarycloud/physics_ai/physics_ai.proto\x1a\x1fproto/ratelimit/ratelimit.proto2\x9f\x04\n\x18PhysicsAiTrainingService\x12\x9e\x01\n\rCreateDataset\x12\x44.luminary.proto.api.v0.luminarycloud.physics_ai.CreateDatasetRequest\x1a\x45.luminary.proto.api.v0.luminarycloud.physics_ai.CreateDatasetResponse\"\x00\x12\xb4\x01\n\x11SubmitTrainingJob\x12H.luminary.proto.api.v0.luminarycloud.physics_ai.SubmitTrainingJobRequest\x1aI.luminary.proto.api.v0.luminarycloud.physics_ai.SubmitTrainingJobResponse\"\n\x8a\xb5\x18\x06\x08\x05\x12\x02\x08\x01\x12\xaa\x01\n\x11\x43\x61ncelTrainingJob\x12H.luminary.proto.api.v0.luminarycloud.physics_ai.CancelTrainingJobRequest\x1aI.luminary.proto.api.v0.luminarycloud.physics_ai.CancelTrainingJobResponse\"\x00\x42\x37Z5luminarycloud.com/core/proto/physicsaitrainingserviceb\x06proto3')
19
20
 
20
21
 
21
22
 
@@ -24,6 +25,8 @@ if _descriptor._USE_C_DESCRIPTORS == False:
24
25
 
25
26
  DESCRIPTOR._options = None
26
27
  DESCRIPTOR._serialized_options = b'Z5luminarycloud.com/core/proto/physicsaitrainingservice'
27
- _PHYSICSAITRAININGSERVICE._serialized_start=163
28
- _PHYSICSAITRAININGSERVICE._serialized_end=362
28
+ _PHYSICSAITRAININGSERVICE.methods_by_name['SubmitTrainingJob']._options = None
29
+ _PHYSICSAITRAININGSERVICE.methods_by_name['SubmitTrainingJob']._serialized_options = b'\212\265\030\006\010\005\022\002\010\001'
30
+ _PHYSICSAITRAININGSERVICE._serialized_start=196
31
+ _PHYSICSAITRAININGSERVICE._serialized_end=739
29
32
  # @@protoc_insertion_point(module_scope)
@@ -15,17 +15,34 @@ class PhysicsAiTrainingServiceStub(object):
15
15
  Args:
16
16
  channel: A grpc.Channel.
17
17
  """
18
+ self.CreateDataset = channel.unary_unary(
19
+ '/luminary.proto.physicsaitrainingservice.PhysicsAiTrainingService/CreateDataset',
20
+ request_serializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CreateDatasetRequest.SerializeToString,
21
+ response_deserializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CreateDatasetResponse.FromString,
22
+ )
18
23
  self.SubmitTrainingJob = channel.unary_unary(
19
24
  '/luminary.proto.physicsaitrainingservice.PhysicsAiTrainingService/SubmitTrainingJob',
20
25
  request_serializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.SubmitTrainingJobRequest.SerializeToString,
21
26
  response_deserializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.SubmitTrainingJobResponse.FromString,
22
27
  )
28
+ self.CancelTrainingJob = channel.unary_unary(
29
+ '/luminary.proto.physicsaitrainingservice.PhysicsAiTrainingService/CancelTrainingJob',
30
+ request_serializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CancelTrainingJobRequest.SerializeToString,
31
+ response_deserializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CancelTrainingJobResponse.FromString,
32
+ )
23
33
 
24
34
 
25
35
  class PhysicsAiTrainingServiceServicer(object):
26
36
  """PhysicsAiTrainingService provides training functionality for Physics AI
27
37
  """
28
38
 
39
+ def CreateDataset(self, request, context):
40
+ """Create a Physics AI dataset
41
+ """
42
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
43
+ context.set_details('Method not implemented!')
44
+ raise NotImplementedError('Method not implemented!')
45
+
29
46
  def SubmitTrainingJob(self, request, context):
30
47
  """Submit a physics AI training job
31
48
  """
@@ -33,14 +50,31 @@ class PhysicsAiTrainingServiceServicer(object):
33
50
  context.set_details('Method not implemented!')
34
51
  raise NotImplementedError('Method not implemented!')
35
52
 
53
+ def CancelTrainingJob(self, request, context):
54
+ """Cancel a physics AI training job
55
+ """
56
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
57
+ context.set_details('Method not implemented!')
58
+ raise NotImplementedError('Method not implemented!')
59
+
36
60
 
37
61
  def add_PhysicsAiTrainingServiceServicer_to_server(servicer, server):
38
62
  rpc_method_handlers = {
63
+ 'CreateDataset': grpc.unary_unary_rpc_method_handler(
64
+ servicer.CreateDataset,
65
+ request_deserializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CreateDatasetRequest.FromString,
66
+ response_serializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CreateDatasetResponse.SerializeToString,
67
+ ),
39
68
  'SubmitTrainingJob': grpc.unary_unary_rpc_method_handler(
40
69
  servicer.SubmitTrainingJob,
41
70
  request_deserializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.SubmitTrainingJobRequest.FromString,
42
71
  response_serializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.SubmitTrainingJobResponse.SerializeToString,
43
72
  ),
73
+ 'CancelTrainingJob': grpc.unary_unary_rpc_method_handler(
74
+ servicer.CancelTrainingJob,
75
+ request_deserializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CancelTrainingJobRequest.FromString,
76
+ response_serializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CancelTrainingJobResponse.SerializeToString,
77
+ ),
44
78
  }
45
79
  generic_handler = grpc.method_handlers_generic_handler(
46
80
  'luminary.proto.physicsaitrainingservice.PhysicsAiTrainingService', rpc_method_handlers)
@@ -52,6 +86,23 @@ class PhysicsAiTrainingService(object):
52
86
  """PhysicsAiTrainingService provides training functionality for Physics AI
53
87
  """
54
88
 
89
+ @staticmethod
90
+ def CreateDataset(request,
91
+ target,
92
+ options=(),
93
+ channel_credentials=None,
94
+ call_credentials=None,
95
+ insecure=False,
96
+ compression=None,
97
+ wait_for_ready=None,
98
+ timeout=None,
99
+ metadata=None):
100
+ return grpc.experimental.unary_unary(request, target, '/luminary.proto.physicsaitrainingservice.PhysicsAiTrainingService/CreateDataset',
101
+ proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CreateDatasetRequest.SerializeToString,
102
+ proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CreateDatasetResponse.FromString,
103
+ options, channel_credentials,
104
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
105
+
55
106
  @staticmethod
56
107
  def SubmitTrainingJob(request,
57
108
  target,
@@ -68,3 +119,20 @@ class PhysicsAiTrainingService(object):
68
119
  proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.SubmitTrainingJobResponse.FromString,
69
120
  options, channel_credentials,
70
121
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
122
+
123
+ @staticmethod
124
+ def CancelTrainingJob(request,
125
+ target,
126
+ options=(),
127
+ channel_credentials=None,
128
+ call_credentials=None,
129
+ insecure=False,
130
+ compression=None,
131
+ wait_for_ready=None,
132
+ timeout=None,
133
+ metadata=None):
134
+ return grpc.experimental.unary_unary(request, target, '/luminary.proto.physicsaitrainingservice.PhysicsAiTrainingService/CancelTrainingJob',
135
+ proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CancelTrainingJobRequest.SerializeToString,
136
+ proto_dot_api_dot_v0_dot_luminarycloud_dot_physics__ai_dot_physics__ai__pb2.CancelTrainingJobResponse.FromString,
137
+ options, channel_credentials,
138
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@@ -10,15 +10,32 @@ class PhysicsAiTrainingServiceStub:
10
10
  """PhysicsAiTrainingService provides training functionality for Physics AI"""
11
11
 
12
12
  def __init__(self, channel: grpc.Channel) -> None: ...
13
+ CreateDataset: grpc.UnaryUnaryMultiCallable[
14
+ luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.CreateDatasetRequest,
15
+ luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.CreateDatasetResponse,
16
+ ]
17
+ """Create a Physics AI dataset"""
13
18
  SubmitTrainingJob: grpc.UnaryUnaryMultiCallable[
14
19
  luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.SubmitTrainingJobRequest,
15
20
  luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.SubmitTrainingJobResponse,
16
21
  ]
17
22
  """Submit a physics AI training job"""
23
+ CancelTrainingJob: grpc.UnaryUnaryMultiCallable[
24
+ luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.CancelTrainingJobRequest,
25
+ luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.CancelTrainingJobResponse,
26
+ ]
27
+ """Cancel a physics AI training job"""
18
28
 
19
29
  class PhysicsAiTrainingServiceServicer(metaclass=abc.ABCMeta):
20
30
  """PhysicsAiTrainingService provides training functionality for Physics AI"""
21
31
 
32
+ @abc.abstractmethod
33
+ def CreateDataset(
34
+ self,
35
+ request: luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.CreateDatasetRequest,
36
+ context: grpc.ServicerContext,
37
+ ) -> luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.CreateDatasetResponse:
38
+ """Create a Physics AI dataset"""
22
39
  @abc.abstractmethod
23
40
  def SubmitTrainingJob(
24
41
  self,
@@ -26,5 +43,12 @@ class PhysicsAiTrainingServiceServicer(metaclass=abc.ABCMeta):
26
43
  context: grpc.ServicerContext,
27
44
  ) -> luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.SubmitTrainingJobResponse:
28
45
  """Submit a physics AI training job"""
46
+ @abc.abstractmethod
47
+ def CancelTrainingJob(
48
+ self,
49
+ request: luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.CancelTrainingJobRequest,
50
+ context: grpc.ServicerContext,
51
+ ) -> luminarycloud._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2.CancelTrainingJobResponse:
52
+ """Cancel a physics AI training job"""
29
53
 
30
54
  def add_PhysicsAiTrainingServiceServicer_to_server(servicer: PhysicsAiTrainingServiceServicer, server: grpc.Server) -> None: ...
luminarycloud/_wrapper.py CHANGED
@@ -103,20 +103,62 @@ class ProtoWrapper(Generic[P]):
103
103
  def getter(field_name: str) -> Any:
104
104
  return lambda self: getattr(self._proto, field_name)
105
105
 
106
- def wrapped_getter(field_name: str, wrapper: type[ProtoWrapperBase]) -> Any:
107
- return lambda self: wrapper(getattr(self._proto, field_name))
106
+ def wrapped_getter(field_name: str, wrapper: type) -> Any:
107
+ def _get(self):
108
+ proto_value = getattr(self._proto, field_name)
109
+
110
+ # If it's a ProtoWrapperBase, use the standard pattern
111
+ if issubclass(wrapper, ProtoWrapperBase):
112
+ return wrapper(proto_value)
113
+
114
+ # If it's Vector3, use the conversion pattern
115
+ if wrapper is Vector3:
116
+ return Vector3(
117
+ x=float(proto_value.x), y=float(proto_value.y), z=float(proto_value.z)
118
+ )
119
+
120
+ # For other types, try the constructor directly
121
+ try:
122
+ return wrapper(proto_value)
123
+ except (TypeError, ValueError):
124
+ # Fallback: return the proto value as-is
125
+ return proto_value
126
+
127
+ return _get
108
128
 
109
129
  # This binds the field name to the setter.
110
130
  def setter(field_name: str) -> Callable[[C, Any], None]:
111
131
  return lambda self, value: setattr(self._proto, field_name, value)
112
132
 
113
- def wrapped_setter(
114
- field_name: str, wrapper: type[ProtoWrapperBase]
115
- ) -> Callable[[C, Any], None]:
133
+ def wrapped_setter(field_name: str, wrapper: type) -> Callable[[C, Any], None]:
116
134
  def _set(self: C, value: Any) -> None:
117
135
  if not isinstance(value, wrapper):
118
136
  raise TypeError(f"{field_name} should be a {wrapper.__name__}")
119
- setattr(self._proto, field_name, value._proto)
137
+
138
+ # If it's a ProtoWrapperBase, extract the _proto
139
+ if issubclass(wrapper, ProtoWrapperBase):
140
+ # For protobuf message fields, use CopyFrom instead of direct assignment
141
+ proto_field = getattr(self._proto, field_name)
142
+ proto_field.CopyFrom(value._proto)
143
+
144
+ # If it's Vector3, convert to protobuf Vector3
145
+ elif wrapper is Vector3:
146
+ proto_field = getattr(self._proto, field_name)
147
+ proto_field.x = float(value.x)
148
+ proto_field.y = float(value.y)
149
+ proto_field.z = float(value.z)
150
+
151
+ # For other types, try direct assignment or conversion
152
+ else:
153
+ try:
154
+ # Try using _to_proto method if it exists
155
+ if hasattr(value, "_to_proto"):
156
+ setattr(self._proto, field_name, value._to_proto())
157
+ else:
158
+ setattr(self._proto, field_name, value)
159
+ except (TypeError, ValueError, AttributeError):
160
+ # Fallback: direct assignment
161
+ setattr(self._proto, field_name, value)
120
162
 
121
163
  return _set
122
164
 
@@ -148,7 +190,11 @@ class ProtoWrapper(Generic[P]):
148
190
  pass
149
191
  else:
150
192
  try:
151
- if issubclass(_type, Enum) or issubclass(_type, ProtoWrapperBase):
193
+ if (
194
+ issubclass(_type, Enum)
195
+ or issubclass(_type, ProtoWrapperBase)
196
+ or _type is Vector3
197
+ ):
152
198
  fget = wrapped_getter(field.name, _type)
153
199
  fset = wrapped_setter(field.name, _type)
154
200
  except TypeError:
@@ -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
@@ -148,6 +148,29 @@ def _update_repeated_field(
148
148
  target_field.extend(new_values)
149
149
 
150
150
 
151
+ def _update_create_op_from_shape(create_op: gpb.Create, shape: Shape) -> None:
152
+ """Helper function to replace the shape in a gpb.Create operation with a shape object.
153
+
154
+ Args:
155
+ create: The gpb.Create to modify
156
+ shape: The shape to add
157
+ """
158
+ if isinstance(shape, Sphere):
159
+ create_op.sphere.CopyFrom(shape._to_proto())
160
+ elif isinstance(shape, Cube):
161
+ create_op.box.CopyFrom(shape._to_proto())
162
+ elif isinstance(shape, Cylinder):
163
+ create_op.cylinder.CopyFrom(shape._to_proto())
164
+ elif isinstance(shape, Torus):
165
+ create_op.torus.CopyFrom(shape._to_proto())
166
+ elif isinstance(shape, Cone):
167
+ create_op.cone.CopyFrom(shape._to_proto())
168
+ elif isinstance(shape, HalfSphere):
169
+ create_op.half_sphere.CopyFrom(shape._to_proto())
170
+ else:
171
+ raise TypeError(f"Unsupported shape type: {type(shape)}")
172
+
173
+
151
174
  def modify_import(
152
175
  feature: gpb.Feature,
153
176
  geometry_url: Optional[str] = None,
@@ -205,22 +228,7 @@ def modify_create(
205
228
  create_op = feature_copy.create
206
229
 
207
230
  if shape is not None:
208
- create_op.ClearField("shape")
209
-
210
- if isinstance(shape, Sphere):
211
- create_op.sphere.CopyFrom(shape._to_proto()) # type: ignore
212
- elif isinstance(shape, Cube):
213
- create_op.box.CopyFrom(shape._to_proto()) # type: ignore
214
- elif isinstance(shape, Cylinder):
215
- create_op.cylinder.CopyFrom(shape._to_proto()) # type: ignore
216
- elif isinstance(shape, Torus):
217
- create_op.torus.CopyFrom(shape._to_proto()) # type: ignore
218
- elif isinstance(shape, Cone):
219
- create_op.cone.CopyFrom(shape._to_proto()) # type: ignore
220
- elif isinstance(shape, HalfSphere):
221
- create_op.half_sphere.CopyFrom(shape._to_proto()) # type: ignore
222
- else:
223
- raise TypeError(f"Unsupported shape type: {type(shape)}")
231
+ _update_create_op_from_shape(create_op, shape)
224
232
 
225
233
  return gpb.Modification(
226
234
  mod_type=gpb.Modification.ModificationType.MODIFICATION_TYPE_UPDATE_FEATURE,
@@ -719,22 +727,7 @@ def modify_farfield(
719
727
  farfield_op = feature_copy.farfield
720
728
 
721
729
  if shape is not None:
722
- create_op = gpb.Create()
723
- if isinstance(shape, Sphere):
724
- create_op.sphere.CopyFrom(shape._to_proto()) # type: ignore
725
- elif isinstance(shape, Cube):
726
- create_op.box.CopyFrom(shape._to_proto()) # type: ignore
727
- elif isinstance(shape, Cylinder):
728
- create_op.cylinder.CopyFrom(shape._to_proto()) # type: ignore
729
- elif isinstance(shape, Torus):
730
- create_op.torus.CopyFrom(shape._to_proto()) # type: ignore
731
- elif isinstance(shape, Cone):
732
- create_op.cone.CopyFrom(shape._to_proto()) # type: ignore
733
- elif isinstance(shape, HalfSphere):
734
- create_op.half_sphere.CopyFrom(shape._to_proto()) # type: ignore
735
- else:
736
- raise TypeError(f"Unsupported shape type: {type(shape)}")
737
- farfield_op.create.CopyFrom(create_op)
730
+ _update_create_op_from_shape(farfield_op.create, shape)
738
731
 
739
732
  if volumes is not None:
740
733
  vol_ids = _volumes_to_int_list(volumes)
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.
@@ -174,17 +178,17 @@ class Geometry(ProtoWrapperBase):
174
178
  """
175
179
  create_proto = gpb.Create()
176
180
  if isinstance(shape, shapes.Sphere):
177
- create_proto.sphere.CopyFrom(shape._to_proto()) # type: ignore
181
+ create_proto.sphere.CopyFrom(shape._to_proto())
178
182
  elif isinstance(shape, shapes.Cube):
179
- create_proto.box.CopyFrom(shape._to_proto()) # type: ignore
183
+ create_proto.box.CopyFrom(shape._to_proto())
180
184
  elif isinstance(shape, shapes.Cylinder):
181
- create_proto.cylinder.CopyFrom(shape._to_proto()) # type: ignore
185
+ create_proto.cylinder.CopyFrom(shape._to_proto())
182
186
  elif isinstance(shape, shapes.Torus):
183
- create_proto.torus.CopyFrom(shape._to_proto()) # type: ignore
187
+ create_proto.torus.CopyFrom(shape._to_proto())
184
188
  elif isinstance(shape, shapes.Cone):
185
- create_proto.cone.CopyFrom(shape._to_proto()) # type: ignore
189
+ create_proto.cone.CopyFrom(shape._to_proto())
186
190
  elif isinstance(shape, shapes.HalfSphere):
187
- create_proto.half_sphere.CopyFrom(shape._to_proto()) # type: ignore
191
+ create_proto.half_sphere.CopyFrom(shape._to_proto())
188
192
  else:
189
193
  raise TypeError(f"Unsupported shape for farfield: {type(shape)}")
190
194
  self._modify(
@@ -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.