Skip to content

Commit ee00276

Browse files
committed
Add support for primitive data handling and RadiationModel functionality
1 parent f363b30 commit ee00276

18 files changed

Lines changed: 5596 additions & 39 deletions

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,8 @@ pytest tests/test_context.py -v
287287
- **Python version matrix**: Tests across Python 3.8-3.12
288288
- **Mock mode validation**: All platforms test PyHelios functionality without native dependencies
289289
- **Native library testing**: Best-effort compilation and testing with actual Helios libraries
290-
- **Quick feedback**: Fast workflows for development branches provide rapid feedback
290+
- **Quick feedback**: Fast workflows for development branches provide rapid feedback
291+
292+
## Error Handling and Fallback Policy
293+
294+
**CRITICAL: PyHelios follows a fail-fast philosophy - never implement silent fallbacks that hide issues from users.** NEVER return fake values (0.0, empty lists, fake IDs), silently catch and ignore exceptions, or continue with misleading fallback functionality when core features fail. Instead, always raise explicit `RuntimeError` or `NotImplementedError` exceptions with clear, actionable error messages that explain what failed, why it failed, and how to fix it. The only acceptable fallbacks are explicit opt-in development modes, documented graceful degradation where users are informed, and safe no-ops like cleaning up `None` resources. **The Golden Rule: If something doesn't work, make it obvious** - users should never wonder why they got unexpected results, but should immediately see clear error messages with specific system requirements and actionable solutions.

Readme.md renamed to README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
<br><br>
2+
3+
<div align="center">
4+
<img src="docs/images/PyHelios_logo_whiteborder.png" alt="" width="300" />
5+
</div>
6+
17
# PyHelios
28

39
Cross-platform Python bindings for [Helios](https://github.com/PlantSimulationLab/Helios) 3D plant simulation library.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"""
2+
PyHelios External Geometry Import Example
3+
4+
This example demonstrates the four main ways to import external geometry into PyHelios:
5+
1. Loading PLY files
6+
2. Loading OBJ files
7+
3. Loading Helios XML files
8+
4. Importing from NumPy arrays (compatible with trimesh, Open3D, etc.)
9+
10+
It also shows how to retrieve primitive information and work with the data.
11+
"""
12+
13+
import numpy as np
14+
from pyhelios import Context, DataTypes
15+
16+
def main():
17+
# Create a PyHelios context
18+
context = Context()
19+
print("PyHelios External Geometry Import Demo")
20+
print("=" * 50)
21+
22+
# ==================== METHOD 1: PLY FILE LOADING ====================
23+
print("\n1. PLY File Loading")
24+
try:
25+
# Simple PLY loading
26+
ply_uuids = context.loadPLY("models/example.ply", silent=True)
27+
print(f" Simple PLY load: {len(ply_uuids)} primitives loaded")
28+
29+
# PLY loading with transformations
30+
origin = DataTypes.vec3(2.0, 0.0, 1.0)
31+
color = DataTypes.RGBcolor(0.8, 0.2, 0.2) # Red tint
32+
rotation = DataTypes.SphericalCoord(1.0, 0.0, 0.0, np.pi/4) # 45 degree rotation
33+
34+
transformed_uuids = context.loadPLY(
35+
filename="models/example.ply",
36+
origin=origin,
37+
height=2.0,
38+
rotation=rotation,
39+
color=color,
40+
upaxis="YUP",
41+
silent=True
42+
)
43+
print(f" Transformed PLY load: {len(transformed_uuids)} primitives loaded")
44+
45+
except Exception as e:
46+
print(f" PLY loading failed (file may not exist): {e}")
47+
48+
# ==================== METHOD 2: OBJ FILE LOADING ====================
49+
print("\n2. OBJ File Loading")
50+
try:
51+
# Simple OBJ loading
52+
obj_uuids = context.loadOBJ("models/example.obj", silent=True)
53+
print(f" Simple OBJ load: {len(obj_uuids)} primitives loaded")
54+
55+
# OBJ loading with full transformations
56+
origin = DataTypes.vec3(-2.0, 0.0, 1.0)
57+
scale = DataTypes.vec3(0.5, 0.5, 0.5) # Half scale
58+
rotation = DataTypes.SphericalCoord(1.0, 0.0, 0.0, -np.pi/6) # -30 degree rotation
59+
color = DataTypes.RGBcolor(0.2, 0.8, 0.2) # Green tint
60+
61+
transformed_obj_uuids = context.loadOBJ(
62+
filename="models/example.obj",
63+
origin=origin,
64+
scale=scale,
65+
rotation=rotation,
66+
color=color,
67+
upaxis="ZUP",
68+
silent=True
69+
)
70+
print(f" Transformed OBJ load: {len(transformed_obj_uuids)} primitives loaded")
71+
72+
except Exception as e:
73+
print(f" OBJ loading failed (file may not exist): {e}")
74+
75+
# ==================== METHOD 3: XML FILE LOADING ====================
76+
print("\n3. Helios XML File Loading")
77+
try:
78+
xml_uuids = context.loadXML("geometry/scene.xml", quiet=True)
79+
print(f" XML load: {len(xml_uuids)} primitives loaded")
80+
81+
except Exception as e:
82+
print(f" XML loading failed (file may not exist): {e}")
83+
84+
# ==================== METHOD 4: NUMPY ARRAY IMPORT ====================
85+
print("\n4. NumPy Array Import (trimesh/Open3D compatible)")
86+
87+
# Create a simple triangular mesh (tetrahedron)
88+
vertices = np.array([
89+
[0.0, 0.0, 0.0], # vertex 0
90+
[1.0, 0.0, 0.0], # vertex 1
91+
[0.5, 1.0, 0.0], # vertex 2
92+
[0.5, 0.5, 1.0] # vertex 3
93+
], dtype=np.float32)
94+
95+
faces = np.array([
96+
[0, 1, 2], # bottom face
97+
[0, 1, 3], # front face
98+
[1, 2, 3], # right face
99+
[0, 2, 3] # left face
100+
], dtype=np.int32)
101+
102+
# Per-vertex colors (RGBA)
103+
colors = np.array([
104+
[1.0, 0.0, 0.0], # red
105+
[0.0, 1.0, 0.0], # green
106+
[0.0, 0.0, 1.0], # blue
107+
[1.0, 1.0, 0.0] # yellow
108+
], dtype=np.float32)
109+
110+
# Import triangles with per-vertex colors
111+
array_uuids = context.addTrianglesFromArrays(vertices, faces, colors)
112+
print(f" NumPy array import: {len(array_uuids)} triangles added")
113+
print(f" Triangle UUIDs: {array_uuids}")
114+
115+
# ==================== TEXTURED TRIANGLE IMPORT ====================
116+
print("\n5. Textured Triangle Import")
117+
118+
# Create a simple quad (2 triangles) with UV coordinates
119+
quad_vertices = np.array([
120+
[3.0, 0.0, 0.0], # bottom-left
121+
[4.0, 0.0, 0.0], # bottom-right
122+
[4.0, 1.0, 0.0], # top-right
123+
[3.0, 1.0, 0.0] # top-left
124+
], dtype=np.float32)
125+
126+
quad_faces = np.array([
127+
[0, 1, 2], # first triangle
128+
[0, 2, 3] # second triangle
129+
], dtype=np.int32)
130+
131+
# UV coordinates for texture mapping
132+
uv_coords = np.array([
133+
[0.0, 0.0], # bottom-left UV
134+
[1.0, 0.0], # bottom-right UV
135+
[1.0, 1.0], # top-right UV
136+
[0.0, 1.0] # top-left UV
137+
], dtype=np.float32)
138+
139+
try:
140+
textured_uuids = context.addTrianglesFromArraysTextured(
141+
vertices=quad_vertices,
142+
faces=quad_faces,
143+
uv_coords=uv_coords,
144+
texture_file="textures/test_texture.png"
145+
)
146+
print(f" Textured triangles: {len(textured_uuids)} triangles added")
147+
print(f" Textured UUIDs: {textured_uuids}")
148+
149+
except Exception as e:
150+
print(f" Textured triangle import failed: {e}")
151+
152+
# ==================== PRIMITIVE INFO RETRIEVAL ====================
153+
print("\n6. Primitive Information Retrieval")
154+
155+
# Get information about the triangles we just created
156+
if array_uuids:
157+
first_triangle_uuid = array_uuids[0]
158+
159+
# Get detailed primitive information
160+
prim_info = context.getPrimitiveInfo(first_triangle_uuid)
161+
print(f" Triangle {first_triangle_uuid} info:")
162+
print(f" Type: {prim_info.primitive_type}")
163+
print(f" Area: {prim_info.area:.4f}")
164+
print(f" Normal: ({prim_info.normal.x:.3f}, {prim_info.normal.y:.3f}, {prim_info.normal.z:.3f})")
165+
print(f" Centroid: ({prim_info.centroid.x:.3f}, {prim_info.centroid.y:.3f}, {prim_info.centroid.z:.3f})")
166+
print(f" Color: RGB({prim_info.color.r:.3f}, {prim_info.color.g:.3f}, {prim_info.color.b:.3f})")
167+
print(f" Vertices:")
168+
for i, vertex in enumerate(prim_info.vertices):
169+
print(f" v{i}: ({vertex.x:.3f}, {vertex.y:.3f}, {vertex.z:.3f})")
170+
171+
# Get info for all primitives
172+
all_primitive_info = context.getAllPrimitiveInfo()
173+
print(f"\n Total primitives in context: {len(all_primitive_info)}")
174+
175+
# Summary by type
176+
type_counts = {}
177+
for info in all_primitive_info:
178+
ptype = info.primitive_type.name
179+
type_counts[ptype] = type_counts.get(ptype, 0) + 1
180+
181+
print(" Primitive counts by type:")
182+
for ptype, count in type_counts.items():
183+
print(f" {ptype}: {count}")
184+
185+
# ==================== PRIMITIVE DATA EXAMPLE ====================
186+
print("\n7. Primitive Data (User-defined key-value pairs)")
187+
print(" Note: Primitive data methods require additional C++ wrapper implementation")
188+
print(" This demonstrates the intended API for user-defined metadata:")
189+
print(" - context.setPrimitiveData(uuid, 'temperature', 25.5)")
190+
print(" - context.setPrimitiveData(uuid, 'material_id', 42)")
191+
print(" - context.setPrimitiveData(uuid, 'source_file', 'imported_model.obj')")
192+
print(" - temp = context.getPrimitiveData(uuid, 'temperature', float)")
193+
194+
# ==================== INTEGRATION WITH POPULAR LIBRARIES ====================
195+
print("\n8. Integration with Popular Python 3D Libraries")
196+
print(" The array format used by PyHelios is compatible with:")
197+
print(" - trimesh: mesh = trimesh.Trimesh(vertices=vertices, faces=faces)")
198+
print(" - Open3D: mesh = o3d.geometry.TriangleMesh()")
199+
print(" mesh.vertices = o3d.utility.Vector3dVector(vertices)")
200+
print(" mesh.triangles = o3d.utility.Vector3iVector(faces)")
201+
print(" - numpy-stl: mesh = numpy_stl.mesh.Mesh(vertices.reshape(-1, 9))")
202+
203+
print("\n" + "=" * 50)
204+
print("External geometry import demo completed!")
205+
print(f"Context contains {context.getPrimitiveCount()} total primitives")
206+
207+
208+
if __name__ == "__main__":
209+
main()

0 commit comments

Comments
 (0)