Skip to content

Commit 156ec89

Browse files
committed
[v0.1.19] 2026-04-16
- Updated helios-core to v1.3.71 ## Plant Architecture - Added `writePlantStructureUSD()` to export a plant as a USD articulated rigid body for NVIDIA IsaacSim physics (capsule links, spherical joints with E*I/L spring/damper drives, organ mass bodies) - Added growth animation export via `registerGrowthFrame()`, `writePlantGrowthUSD()`, `clearGrowthFrames()`, and `getGrowthFrameCount()` for time-sampled USD animations importable into Blender ## Context - Added `updateTimeseriesData()` method to replace the value of an existing timeseries data point at a specified (date, time) - Added compound-object geometry queries: `getObjectType()`, `getObjectCenter()`, `getObjectBoundingBox()`, `getObjectPrimitiveUUIDs()` (single/list/nested), plus per-type getters for tile, sphere, box, disk, tube, and cone objects (center, size, subdivision count, normal, vertices, radius, node/radius data, axis, length, volume) - Added primitive geometry queries: `getPatchCenter()`, `getPatchSize()`, `getTriangleVertex()`, `getVoxelCenter()`, `getVoxelSize()`, `getPatchCount()`, `getTriangleCount()`, `getPrimitiveBoundingBox()` (single UUID or list) - Added `setPrimitiveColor()` for mutating the color of one primitive or a list of primitives, accepting either `RGBcolor` or `RGBAcolor` - Added `clearPrimitiveData()` and `listPrimitiveData()` for removing and inspecting per-primitive data fields - Added domain cropping: `cropDomainX()`, `cropDomainY()`, `cropDomainZ()`, and `cropDomain()` to restrict all primitives (or a supplied UUID list) to given XYZ bounds
1 parent d8c4436 commit 156ec89

13 files changed

+2849
-3
lines changed

docs/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
# [v0.1.19] 2026-04-16
4+
5+
- Updated helios-core to v1.3.71
6+
7+
## Plant Architecture
8+
- Added `writePlantStructureUSD()` to export a plant as a USD articulated rigid body for NVIDIA IsaacSim physics (capsule links, spherical joints with E*I/L spring/damper drives, organ mass bodies)
9+
- Added growth animation export via `registerGrowthFrame()`, `writePlantGrowthUSD()`, `clearGrowthFrames()`, and `getGrowthFrameCount()` for time-sampled USD animations importable into Blender
10+
11+
## Context
12+
- Added `updateTimeseriesData()` method to replace the value of an existing timeseries data point at a specified (date, time)
13+
- Added compound-object geometry queries: `getObjectType()`, `getObjectCenter()`, `getObjectBoundingBox()`, `getObjectPrimitiveUUIDs()` (single/list/nested), plus per-type getters for tile, sphere, box, disk, tube, and cone objects (center, size, subdivision count, normal, vertices, radius, node/radius data, axis, length, volume)
14+
- Added primitive geometry queries: `getPatchCenter()`, `getPatchSize()`, `getTriangleVertex()`, `getVoxelCenter()`, `getVoxelSize()`, `getPatchCount()`, `getTriangleCount()`, `getPrimitiveBoundingBox()` (single UUID or list)
15+
- Added `setPrimitiveColor()` for mutating the color of one primitive or a list of primitives, accepting either `RGBcolor` or `RGBAcolor`
16+
- Added `clearPrimitiveData()` and `listPrimitiveData()` for removing and inspecting per-primitive data fields
17+
- Added domain cropping: `cropDomainX()`, `cropDomainY()`, `cropDomainZ()`, and `cropDomain()` to restrict all primitives (or a supplied UUID list) to given XYZ bounds
18+
319
# [v0.1.18] 2026-03-18
420

521
- Updated helios-core to v1.3.70

docs/plugin_plantarchitecture.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,12 +728,14 @@ PlantArchitecture provides comprehensive file I/O capabilities to save and load
728728

729729
### Overview
730730

731-
Four file I/O methods are available:
731+
The following file I/O methods are available:
732732

733733
1. **`writePlantStructureXML()`**: Save complete plant structure to XML for later loading
734734
2. **`readPlantStructureXML()`**: Load saved plant structures from XML files
735735
3. **`writePlantMeshVertices()`**: Export all mesh vertices for external processing
736736
4. **`writeQSMCylinderFile()`**: Export to TreeQSM format for biomechanical analysis
737+
5. **`writePlantStructureUSD()`**: Export plant structure as a USD articulated rigid body for NVIDIA IsaacSim physics simulation
738+
6. **`registerGrowthFrame()`** / **`writePlantGrowthUSD()`** / **`clearGrowthFrames()`** / **`getGrowthFrameCount()`**: Capture and export per-step growth snapshots as a time-sampled USD animation file (importable into Blender)
737739

738740
All methods work with both string paths and `pathlib.Path` objects, and correctly handle relative/absolute paths.
739741

@@ -887,6 +889,66 @@ The exported file contains tab-separated values with the following information f
887889
- Tree architecture studies
888890
- Integration with TreeQSM analysis tools
889891

892+
### Export USD Articulated Rigid Body (IsaacSim Physics)
893+
894+
Export the plant structure as a PhysX articulation in USDA (ASCII USD) format for NVIDIA IsaacSim physics simulation. Each tube segment becomes a capsule-shaped rigid link connected by spherical joints whose local frames encode the rest-pose orientation. Spring/damper drives are derived from beam bending stiffness (`K = E*I/L`). Leaves, fruits, and flowers are represented as mass bodies attached by spring links.
895+
896+
**Basic Usage:**
897+
898+
```python
899+
from pyhelios import Context, PlantArchitecture
900+
from pyhelios.types import *
901+
902+
with Context() as context:
903+
with PlantArchitecture(context) as plantarch:
904+
plantarch.loadPlantModelFromLibrary("almond")
905+
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=50.0)
906+
907+
# Default physics parameters
908+
plantarch.writePlantStructureUSD(plant_id, "almond.usda")
909+
910+
# Custom physics parameters
911+
plantarch.writePlantStructureUSD(
912+
plant_id, "almond_custom.usda",
913+
elastic_modulus=8e9, # Young's modulus (Pa)
914+
wood_density=750.0, # kg/m^3
915+
damping_ratio=0.15,
916+
leaf_mass_per_area=0.04, # kg/m^2
917+
fruit_mass=0.005, # kg
918+
)
919+
```
920+
921+
### Growth Animation Export (USD for Blender)
922+
923+
Capture per-step plant geometry snapshots during a growth simulation and export them as a time-sampled USDA animation that imports directly into Blender. Organs that appear during growth are toggled visible at the appropriate frame.
924+
925+
**Basic Usage:**
926+
927+
```python
928+
from pyhelios import Context, PlantArchitecture
929+
from pyhelios.types import *
930+
931+
with Context() as context:
932+
with PlantArchitecture(context) as plantarch:
933+
plantarch.loadPlantModelFromLibrary("bean")
934+
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=5.0)
935+
936+
# Capture a frame after each growth step
937+
for _ in range(10):
938+
plantarch.advanceTime(2.0)
939+
plantarch.registerGrowthFrame(plant_id)
940+
941+
print(f"Captured {plantarch.getGrowthFrameCount(plant_id)} frames")
942+
943+
# Export as a 1-second-per-frame animation
944+
plantarch.writePlantGrowthUSD(plant_id, "bean_growth.usda", seconds_per_frame=1.0)
945+
946+
# Reset frame storage when done
947+
plantarch.clearGrowthFrames(plant_id)
948+
```
949+
950+
This export is visual-only — no physics prims, joints, or collision shapes are written. For physics simulation, use `writePlantStructureUSD()` instead.
951+
890952
### Path Handling
891953

892954
All file I/O methods accept both string paths and `pathlib.Path` objects, and correctly handle relative/absolute paths while preserving the user's working directory.

helios-core

Submodule helios-core updated 285 files

native/include/pyhelios_wrapper_context.h

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,25 @@ PYHELIOS_API void getDate(helios::Context* context, int* day, int* month, int* y
16721672
PYHELIOS_API void addTimeseriesData(helios::Context* context, const char* label, float value,
16731673
int day, int month, int year, int hour, int minute, int second);
16741674

1675+
/**
1676+
* @brief Update the value of an existing timeseries data point
1677+
* @param context Pointer to the Context
1678+
* @param label Name of the timeseries variable
1679+
* @param day Day of month (1-31) of the existing point
1680+
* @param month Month (1-12) of the existing point
1681+
* @param year Year of the existing point
1682+
* @param hour Hour of day (0-23) of the existing point
1683+
* @param minute Minute of hour (0-59) of the existing point
1684+
* @param second Second of minute (0-59) of the existing point
1685+
* @param new_value Replacement value to assign at the specified timestamp
1686+
*
1687+
* Throws a runtime error if the variable does not exist or if no point with the
1688+
* specified (date, time) is found. Use addTimeseriesData() to append a new point.
1689+
*/
1690+
PYHELIOS_API void updateTimeseriesData(helios::Context* context, const char* label,
1691+
int day, int month, int year, int hour, int minute, int second,
1692+
float new_value);
1693+
16751694
/**
16761695
* @brief Set the Context date and time from a timeseries data point index
16771696
* @param context Pointer to the Context
@@ -2147,6 +2166,89 @@ PYHELIOS_API unsigned int* filterPrimitivesByDataFloat(helios::Context* context,
21472166
PYHELIOS_API unsigned int* filterPrimitivesByDataInt(helios::Context* context, unsigned int* uuids, unsigned int count, const char* label, int value, const char* comparator, unsigned int* result_count);
21482167
PYHELIOS_API unsigned int* filterPrimitivesByDataString(helios::Context* context, unsigned int* uuids, unsigned int count, const char* label, const char* value, unsigned int* result_count);
21492168

2169+
// ==================== Object Geometry Queries ====================
2170+
PYHELIOS_API unsigned int getObjectType(helios::Context* context, unsigned int objID);
2171+
PYHELIOS_API float* getObjectCenter(helios::Context* context, unsigned int objID);
2172+
PYHELIOS_API void getObjectBoundingBox(helios::Context* context, unsigned int objID, float* min_corner, float* max_corner);
2173+
PYHELIOS_API void getObjectBoundingBox_batch(helios::Context* context, unsigned int* objIDs, unsigned int count, float* min_corner, float* max_corner);
2174+
PYHELIOS_API unsigned int* getObjectPrimitiveUUIDs_batch(helios::Context* context, unsigned int* objIDs, unsigned int count, unsigned int* size);
2175+
PYHELIOS_API unsigned int* getObjectPrimitiveUUIDs_nested(helios::Context* context, unsigned int* flat_objIDs, unsigned int* inner_counts, unsigned int outer_count, unsigned int* size);
2176+
2177+
// Tile
2178+
PYHELIOS_API float getTileObjectAreaRatio(helios::Context* context, unsigned int objID);
2179+
PYHELIOS_API float* getTileObjectAreaRatio_batch(helios::Context* context, unsigned int* objIDs, unsigned int count, unsigned int* size);
2180+
PYHELIOS_API float* getTileObjectCenter(helios::Context* context, unsigned int objID);
2181+
PYHELIOS_API float* getTileObjectSize(helios::Context* context, unsigned int objID);
2182+
PYHELIOS_API int* getTileObjectSubdivisionCount(helios::Context* context, unsigned int objID);
2183+
PYHELIOS_API float* getTileObjectNormal(helios::Context* context, unsigned int objID);
2184+
PYHELIOS_API float* getTileObjectTextureUV(helios::Context* context, unsigned int objID, unsigned int* size);
2185+
PYHELIOS_API float* getTileObjectVertices(helios::Context* context, unsigned int objID, unsigned int* size);
2186+
2187+
// Sphere
2188+
PYHELIOS_API float* getSphereObjectCenter(helios::Context* context, unsigned int objID);
2189+
PYHELIOS_API float* getSphereObjectRadius(helios::Context* context, unsigned int objID);
2190+
PYHELIOS_API unsigned int getSphereObjectSubdivisionCount(helios::Context* context, unsigned int objID);
2191+
PYHELIOS_API float getSphereObjectVolume(helios::Context* context, unsigned int objID);
2192+
2193+
// Box
2194+
PYHELIOS_API float* getBoxObjectCenter(helios::Context* context, unsigned int objID);
2195+
PYHELIOS_API float* getBoxObjectSize(helios::Context* context, unsigned int objID);
2196+
PYHELIOS_API int* getBoxObjectSubdivisionCount(helios::Context* context, unsigned int objID);
2197+
PYHELIOS_API float getBoxObjectVolume(helios::Context* context, unsigned int objID);
2198+
2199+
// Disk
2200+
PYHELIOS_API float* getDiskObjectCenter(helios::Context* context, unsigned int objID);
2201+
PYHELIOS_API float* getDiskObjectSize(helios::Context* context, unsigned int objID);
2202+
PYHELIOS_API unsigned int getDiskObjectSubdivisionCount(helios::Context* context, unsigned int objID);
2203+
2204+
// Tube
2205+
PYHELIOS_API unsigned int getTubeObjectSubdivisionCount(helios::Context* context, unsigned int objID);
2206+
PYHELIOS_API unsigned int getTubeObjectNodeCount(helios::Context* context, unsigned int objID);
2207+
PYHELIOS_API float* getTubeObjectNodes(helios::Context* context, unsigned int objID, unsigned int* size);
2208+
PYHELIOS_API float* getTubeObjectNodeRadii(helios::Context* context, unsigned int objID, unsigned int* size);
2209+
PYHELIOS_API float* getTubeObjectNodeColors(helios::Context* context, unsigned int objID, unsigned int* size);
2210+
PYHELIOS_API float getTubeObjectVolume(helios::Context* context, unsigned int objID);
2211+
PYHELIOS_API float getTubeObjectSegmentVolume(helios::Context* context, unsigned int objID, unsigned int segment_index);
2212+
2213+
// Cone
2214+
PYHELIOS_API unsigned int getConeObjectSubdivisionCount(helios::Context* context, unsigned int objID);
2215+
PYHELIOS_API float* getConeObjectNodes(helios::Context* context, unsigned int objID, unsigned int* size);
2216+
PYHELIOS_API float* getConeObjectNodeRadii(helios::Context* context, unsigned int objID, unsigned int* size);
2217+
PYHELIOS_API float* getConeObjectNode(helios::Context* context, unsigned int objID, int number);
2218+
PYHELIOS_API float getConeObjectNodeRadius(helios::Context* context, unsigned int objID, int number);
2219+
PYHELIOS_API float* getConeObjectAxisUnitVector(helios::Context* context, unsigned int objID);
2220+
PYHELIOS_API float getConeObjectLength(helios::Context* context, unsigned int objID);
2221+
PYHELIOS_API float getConeObjectVolume(helios::Context* context, unsigned int objID);
2222+
2223+
// ==================== Primitive Geometry Queries ====================
2224+
PYHELIOS_API float* getPatchCenter(helios::Context* context, unsigned int uuid);
2225+
PYHELIOS_API float* getPatchSize(helios::Context* context, unsigned int uuid);
2226+
PYHELIOS_API float* getTriangleVertex(helios::Context* context, unsigned int uuid, unsigned int number);
2227+
PYHELIOS_API float* getVoxelCenter(helios::Context* context, unsigned int uuid);
2228+
PYHELIOS_API float* getVoxelSize(helios::Context* context, unsigned int uuid);
2229+
PYHELIOS_API unsigned int getPatchCount(helios::Context* context, bool include_hidden);
2230+
PYHELIOS_API unsigned int getTriangleCount(helios::Context* context, bool include_hidden);
2231+
PYHELIOS_API void getPrimitiveBoundingBox(helios::Context* context, unsigned int uuid, float* min_corner, float* max_corner);
2232+
PYHELIOS_API void getPrimitiveBoundingBox_batch(helios::Context* context, unsigned int* uuids, unsigned int count, float* min_corner, float* max_corner);
2233+
2234+
// ==================== Primitive Color Mutation ====================
2235+
PYHELIOS_API void setPrimitiveColor(helios::Context* context, unsigned int uuid, float* color);
2236+
PYHELIOS_API void setPrimitiveColor_batch(helios::Context* context, unsigned int* uuids, unsigned int count, float* color);
2237+
PYHELIOS_API void setPrimitiveColorRGBA(helios::Context* context, unsigned int uuid, float* color);
2238+
PYHELIOS_API void setPrimitiveColorRGBA_batch(helios::Context* context, unsigned int* uuids, unsigned int count, float* color);
2239+
2240+
// ==================== Primitive Data Introspection / Cleanup ====================
2241+
PYHELIOS_API void clearPrimitiveDataByLabel(helios::Context* context, unsigned int uuid, const char* label);
2242+
PYHELIOS_API void clearPrimitiveDataByLabel_batch(helios::Context* context, unsigned int* uuids, unsigned int count, const char* label);
2243+
PYHELIOS_API const char** listPrimitiveData(helios::Context* context, unsigned int uuid, unsigned int* count);
2244+
2245+
// ==================== Domain Cropping ====================
2246+
PYHELIOS_API void cropDomainX(helios::Context* context, float* xbounds);
2247+
PYHELIOS_API void cropDomainY(helios::Context* context, float* ybounds);
2248+
PYHELIOS_API void cropDomainZ(helios::Context* context, float* zbounds);
2249+
PYHELIOS_API void cropDomainXYZ(helios::Context* context, float* xbounds, float* ybounds, float* zbounds);
2250+
PYHELIOS_API unsigned int* cropDomainByUUIDs(helios::Context* context, unsigned int* uuids, unsigned int count, float* xbounds, float* ybounds, float* zbounds, unsigned int* out_size);
2251+
21502252
#ifdef __cplusplus
21512253
}
21522254
#endif

native/include/pyhelios_wrapper_plantarchitecture.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ PYHELIOS_API unsigned int* getPlantCollisionRelevantObjectIDs(PlantArchitecture*
5454
PYHELIOS_API int writePlantMeshVertices(PlantArchitecture* plantarch, unsigned int plantID, const char* filename);
5555
PYHELIOS_API int writePlantStructureXML(PlantArchitecture* plantarch, unsigned int plantID, const char* filename);
5656
PYHELIOS_API int writeQSMCylinderFile(PlantArchitecture* plantarch, unsigned int plantID, const char* filename);
57+
PYHELIOS_API int writePlantStructureUSD(PlantArchitecture* plantarch, unsigned int plantID, const char* filename,
58+
float elastic_modulus, float wood_density, float damping_ratio,
59+
float static_friction, float dynamic_friction, float restitution,
60+
float organ_spring_stiffness, float organ_spring_damping,
61+
float leaf_mass_per_area, float fruit_mass, float flower_mass,
62+
unsigned int solver_position_iterations, float min_segment_length);
63+
PYHELIOS_API int registerGrowthFrame(PlantArchitecture* plantarch, unsigned int plantID, float min_segment_length);
64+
PYHELIOS_API int writePlantGrowthUSD(PlantArchitecture* plantarch, unsigned int plantID, const char* filename, float seconds_per_frame);
65+
PYHELIOS_API int clearGrowthFrames(PlantArchitecture* plantarch, unsigned int plantID);
66+
PYHELIOS_API unsigned int getGrowthFrameCount(PlantArchitecture* plantarch, unsigned int plantID);
5767
PYHELIOS_API int readPlantStructureXML(PlantArchitecture* plantarch, const char* filename, bool quiet, unsigned int** plant_ids, int* num_plants);
5868

5969
// Parameter management functions

0 commit comments

Comments
 (0)