Overview
PlantArchitecture provides advanced plant structure and architecture modeling with a comprehensive library of 28 procedural plant models. This plugin enables time-based plant growth simulation, procedural plant generation, and plant community modeling for scientific applications including agriculture, forestry, and ecological research.
The plugin includes pre-built models for major agricultural crops (bean, cowpea, maize, rice, soybean, wheat), fruit trees (almond, apple, olive, walnut), and other plant species with biologically-accurate growth parameters and morphological characteristics.
System Requirements
- Platforms: Windows, Linux, macOS
- Dependencies: Extensive asset library (textures, OBJ models, configuration files)
- GPU: Not required
- Memory: Moderate memory usage scales with plant complexity and canopy size
- Assets: Large asset collection (~100MB) with textures, 3D models, and species parameters
Installation
Build with PlantArchitecture
PlantArchitecture is included in default PyHelios builds. To build explicitly:
# Using interactive selection
build_scripts/build_helios --interactive
# Explicit selection
build_scripts/build_helios --plugins plantarchitecture
# Clean build
build_scripts/build_helios --clean --plugins plantarchitecture
# Check if available
python -c "from pyhelios.plugins import print_plugin_status; print_plugin_status()"
Verify Installation
from pyhelios import PlantArchitecture
if is_plantarchitecture_available():
print("PlantArchitecture is available")
else:
print("PlantArchitecture not available - rebuild required")
High-level interface for plant architecture modeling and procedural plant generation.
Quick Start
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
models = plantarch.getAvailablePlantModels()
print(f"Available models: {models}")
plantarch.loadPlantModelFromLibrary("bean")
position = vec3(0, 0, 0)
age = 30.0
plant_id = plantarch.buildPlantInstanceFromLibrary(position, age)
print(f"Created plant ID: {plant_id}")
plantarch.advanceTime(10.0)
Available Plant Models
PlantArchitecture includes 28 scientifically-validated plant models:
Field Crops:
"bean" - Common bean with climbing growth habit
"cowpea" - Cowpea with determinate growth pattern
"maize" - Corn with C4 photosynthetic characteristics
"rice" - Rice with tillering growth pattern
"sorghum" - Sorghum grain crop
"soybean" - Soybean with determinate/indeterminate varieties
"wheat" - Wheat with tiller development
Trees:
"almond" - Almond tree with seasonal growth patterns
"apple" - Apple tree with standard varieties
"apple_fruitingwall" - Apple fruiting wall (specialized high-density training system)
"easternredbud" - Ornamental tree
"olive" - Olive tree with Mediterranean characteristics
"pistachio" - Pistachio with alternating bearing patterns
"walnut" - Walnut tree with complex branching
Vegetables:
"asparagus" - Asparagus perennial vegetable crop
"butterlettuce" - Lettuce with rosette growth form
"capsicum" - Bell pepper with bush growth habit
"cherrytomato" - Cherry tomato variant
"strawberry" - Strawberry with runner propagation
"sugarbeet" - Sugar beet root crop
"tomato" - Tomato with determinate/indeterminate growth
Weeds:
"bindweed" - Invasive vine species
"cheeseweed" - Common weed species
"groundcherryweed" - Weed species related to tomato and tomatillo
"puncturevine" - Prostrate weed species
Vines and Ornamentals:
"bougainvillea" - Ornamental flowering vine with vibrant bracts
"grapevine_VSP" - Grapevine with vertical shoot positioned trellis
"grapevine_Wye" - Grapevine with Wye trellis (quadrilateral)
Examples
Basic Plant Creation
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("bean")
position = vec3(0, 0, 0)
age = 20.0
plant_id = plantarch.buildPlantInstanceFromLibrary(position, age)
print(f"Created bean plant {plant_id} at age {age} days")
Plant Canopy Generation
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("maize")
canopy_center = vec3(0, 0, 0)
plant_spacing = vec2(0.75, 0.75)
plant_count = int2(5, 5)
age = 45.0
plant_ids = plantarch.buildPlantCanopyFromLibrary(
canopy_center, plant_spacing, plant_count, age
)
print(f"Created canopy with {len(plant_ids)} maize plants")
print(f"Plant IDs: {plant_ids}")
Time-Based Growth Simulation
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("soybean")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), 15.0)
growth_days = [5, 10, 5, 8]
for days in growth_days:
print(f"Advancing {days} days...")
plantarch.advanceTime(days)
object_ids = plantarch.getAllPlantObjectIDs(plant_id)
uuids = plantarch.getAllPlantUUIDs(plant_id)
print(f" Plant now has {len(object_ids)} objects, {len(uuids)} primitives")
Multi-Species Simulation
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
species_positions = [
("bean", vec3(-1, 0, 0), 25.0),
("maize", vec3(0, 0, 0), 30.0),
("soybean", vec3(1, 0, 0), 20.0)
]
plant_ids = []
for species, position, age in species_positions:
plantarch.loadPlantModelFromLibrary(species)
plant_id = plantarch.buildPlantInstanceFromLibrary(position, age)
plant_ids.append((species, plant_id))
print(f"Created {species} plant (ID: {plant_id}) at age {age} days")
plantarch.advanceTime(21.0)
for species, plant_id in plant_ids:
primitives = plantarch.getAllPlantUUIDs(plant_id)
print(f"{species} plant {plant_id}: {len(primitives)} primitives")
Error Handling
from pyhelios import Context, PlantArchitecture, PlantArchitectureError
with Context() as context:
try:
with PlantArchitecture(context) as plantarch:
try:
plantarch.loadPlantModelFromLibrary("nonexistent_plant")
except PlantArchitectureError as e:
print(f"Model loading error: {e}")
plantarch.loadPlantModelFromLibrary("bean")
try:
plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), -5.0)
except ValueError as e:
print(f"Parameter validation error: {e}")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), 30.0)
print(f"Successfully created plant {plant_id}")
except PlantArchitectureError as e:
print(f"Plugin error: {e}")
Species-Specific Characteristics
Different plant models have unique biological characteristics:
- Annual crops (bean, maize, wheat): Complete lifecycle in one growing season
- Perennial trees (almond, olive, walnut): Multi-year growth patterns with seasonal cycles
- Determinant growth (some beans, tomatoes): Defined growth endpoint
- Indeterminant growth (some tomatoes, vines): Continuous growth under favorable conditions
Collision Detection
PlantArchitecture integrates advanced collision detection capabilities to enable realistic plant growth that responds to obstacles and other plants. The collision detection system uses cone-based ray tracing to guide plant growth away from obstacles while maintaining natural plant architecture.
Overview
The collision detection system provides two primary modes:
- Soft Collision Avoidance: Guides plant growth to naturally minimize collisions with itself and other plants while tending to fill open space (space colonization)
- Hard Obstacle Avoidance: Strictly prevents plant growth through solid boundaries like ground, walls, or buildings
Both modes use a "perception cone" at the shoot apex to detect obstacles and guide growth direction. Ray-tracing calculations determine objects within the cone's field of view, allowing the plant to react appropriately.
Perception Cone Parameters
The perception cone is the fundamental mechanism for collision detection. Key parameters control its behavior:
- View Half-Angle (degrees, 0-180): Field of view of the detection cone. Default: 80°
- Wider angles detect more obstacles but increase computational cost
- Narrower angles focus detection but may miss nearby obstacles
- Look-Ahead Distance (meters): How far ahead the plant "looks" for obstacles. Default: 0.1m
- Longer distances detect distant obstacles earlier
- Shorter distances are suitable for dense canopies
- Sample Count (rays): Number of rays launched within the cone. Default: 256
- More samples provide better accuracy but reduce performance
- Fewer samples improve speed but may miss small obstacles
- Inertia Weight (0-1): How strongly growth maintains previous direction. Default: 0.4
- Lower values (e.g., 0.2) make growth more responsive to obstacles
- Higher values (e.g., 0.6) make growth smoother and more gradual
Soft Collision Avoidance
Soft collision avoidance guides plant growth to minimize collisions while maintaining natural architecture. Growth direction is adjusted toward the largest gap detected within the perception cone.
Basic Usage:
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("bean")
plantarch.enableSoftCollisionAvoidance()
plant_ids = plantarch.buildPlantCanopyFromLibrary(
canopy_center=vec3(0, 0, 0),
plant_spacing=vec2(0.3, 0.3),
plant_count=int2(3, 3),
age=10.0
)
plantarch.advanceTime(30.0)
Customized Parameters:
plantarch.setSoftCollisionAvoidanceParameters(
view_half_angle_deg=60.0,
look_ahead_distance=0.05,
sample_count=512,
inertia_weight=0.3
)
plantarch.enableSoftCollisionAvoidance()
Target-Specific Collision Detection:
pole_uuids = [context.addPatch(vec3(0, 0, 0.5), size=(0.1, 1))]
plantarch.enableSoftCollisionAvoidance(
target_object_UUIDs=pole_uuids,
enable_petiole_collision=True,
enable_fruit_collision=False
)
Hard Obstacle Avoidance
Hard obstacle avoidance strictly prevents plant growth through solid boundaries. When an obstacle is detected within the avoidance distance, growth is redirected perpendicular to the obstacle surface.
Basic Usage:
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
ground_uuid = context.addPatch(
center=vec3(0, 0, 0),
size=(5, 5),
color=RGBcolor(0.6, 0.4, 0.2)
)
wall_uuid = context.addPatch(
center=vec3(1.5, 0, 0.5),
size=(0.1, 3)
)
plantarch.loadPlantModelFromLibrary("tomato")
plantarch.enableSolidObstacleAvoidance(
obstacle_UUIDs=[ground_uuid, wall_uuid],
avoidance_distance=0.3
)
plant_id = plantarch.buildPlantInstanceFromLibrary(
base_position=vec3(0.5, 0, 0),
age=10.0
)
plantarch.advanceTime(25.0)
With Fruit Adjustment:
ground = context.addPatch(vec3(0, 0, 0), size=(5, 5))
wall = context.addPatch(vec3(1.5, 0, 0.5), size=(0.1, 3))
plantarch.enableSolidObstacleAvoidance(
obstacle_UUIDs=[ground, wall],
avoidance_distance=0.4,
enable_fruit_adjustment=True,
enable_obstacle_pruning=False
)
Performance Optimization
Collision detection can be computationally expensive. Several optimization techniques improve performance:
Static Obstacle Marking
Mark non-moving geometry as static to enable BVH (Bounding Volume Hierarchy) optimization:
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
ground = context.addPatch(vec3(0, 0, 0), size=(10, 10))
building = context.addPatch(vec3(3, 3, 1), size=(2, 2))
static_uuids = [ground, building]
plantarch.loadPlantModelFromLibrary("soybean")
plantarch.enableSoftCollisionAvoidance()
plantarch.setStaticObstacles(static_uuids)
plant_ids = plantarch.buildPlantCanopyFromLibrary(
canopy_center=vec3(0, 0, 0),
plant_spacing=vec2(0.5, 0.5),
plant_count=int2(5, 5),
age=15.0
)
plantarch.advanceTime(30.0)
Key Points:
- Static obstacles should not move during simulation
- BVH is built once and reused for all ray queries
- Significantly improves performance in complex scenes
- Must call
setStaticObstacles() AFTER enableSoftCollisionAvoidance()
Organ Filtering
Selectively include organ types in collision detection to balance accuracy and performance:
plantarch.setCollisionRelevantOrgans(
include_internodes=True,
include_leaves=True,
include_petioles=False,
include_flowers=False,
include_fruit=False
)
plantarch.enableSoftCollisionAvoidance()
Default Configuration:
- Only leaves are included by default (best performance)
- Internodes, petioles, flowers, and fruit are excluded
Recommendations:
- For most crops: leaves only (default)
- For woody plants: leaves + internodes
- For dense canopies: leaves + internodes + petioles
- Always benchmark performance impact when adding organ types
Querying Collision-Relevant Geometry
Retrieve which objects are participating in collision detection for visualization or debugging:
collision_obj_ids = plantarch.getPlantCollisionRelevantObjectIDs(plant_id)
print(f"Plant {plant_id} has {len(collision_obj_ids)} collision-relevant objects")
for obj_id in collision_obj_ids:
context.setObjectColor(obj_id, RGBcolor(1, 0, 0))
Disabling Collision Detection
Turn off collision detection when not needed:
plantarch.disableCollisionDetection()
plantarch.advanceTime(10.0)
Complete Workflow Example
Realistic scenario combining all collision detection features:
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
ground = context.addPatch(vec3(0, 0, 0), size=(8, 8))
building = context.addPatch(vec3(2, 2, 1), size=(2, 2))
fence_uuids = [
context.addPatch(vec3(-3, y, 0.5), size=(0.1, 1))
for y in range(-3, 4)
]
static_uuids = [ground, building] + fence_uuids
plantarch.loadPlantModelFromLibrary("cowpea")
plantarch.setSoftCollisionAvoidanceParameters(
view_half_angle_deg=80.0,
look_ahead_distance=0.1,
sample_count=256,
inertia_weight=0.4
)
plantarch.setCollisionRelevantOrgans(
include_internodes=True,
include_leaves=True,
include_petioles=False,
include_flowers=False,
include_fruit=False
)
plantarch.enableSoftCollisionAvoidance()
plantarch.setStaticObstacles(static_uuids)
plantarch.enableSolidObstacleAvoidance(
obstacle_UUIDs=[building] + fence_uuids,
avoidance_distance=0.4,
enable_fruit_adjustment=True
)
plant_ids = plantarch.buildPlantCanopyFromLibrary(
canopy_center=vec3(-1, -1, 0),
plant_spacing=vec2(0.5, 0.5),
plant_count=int2(4, 4),
age=8.0
)
plantarch.advanceTime(35.0)
context.writeOBJ("collision_example.obj")
Performance Considerations
Collision detection computational cost scales with:
- Scene complexity: Number of primitives in the scene
- Perception cone size: Larger cones require more ray queries
- Sample count: More rays = more intersection tests
- Plant density: More plants = more collision checks
- Organ inclusion: More organ types = larger BVH
Performance Tips:
- Use static obstacles for non-moving geometry (ground, buildings)
- Filter organs - include only necessary organ types (default: leaves only)
- Tune cone parameters - smaller cones and fewer samples for dense canopies
- Enable selectively - only activate collision detection when needed
- Batch growth - use larger time steps (e.g., 5-10 days) rather than daily increments
Example Performance Settings:
plantarch.setSoftCollisionAvoidanceParameters(
view_half_angle_deg=90.0,
look_ahead_distance=0.15,
sample_count=512,
inertia_weight=0.4
)
plantarch.setSoftCollisionAvoidanceParameters(
view_half_angle_deg=80.0,
look_ahead_distance=0.1,
sample_count=256,
inertia_weight=0.4
)
plantarch.setSoftCollisionAvoidanceParameters(
view_half_angle_deg=60.0,
look_ahead_distance=0.08,
sample_count=128,
inertia_weight=0.5
)
Common Patterns
Pattern 1: Dense Canopy with Self-Avoidance
plantarch.setSoftCollisionAvoidanceParameters(
view_half_angle_deg=70.0,
look_ahead_distance=0.08,
sample_count=256,
inertia_weight=0.3
)
plantarch.setCollisionRelevantOrgans(
include_internodes=True,
include_leaves=True,
include_petioles=False,
include_flowers=False,
include_fruit=False
)
plantarch.enableSoftCollisionAvoidance()
Pattern 2: Greenhouse with Infrastructure
posts = []
for x in [-2, 2]:
for y in [-2, 2]:
posts.append(context.addPatch(vec3(x, y, 1), size=(0.1, 2)))
plantarch.enableSoftCollisionAvoidance()
plantarch.setStaticObstacles(posts)
plantarch.enableSolidObstacleAvoidance(
obstacle_UUIDs=posts,
avoidance_distance=0.3
)
Pattern 3: Field with Ground Clipping
ground = context.addPatch(vec3(0, 0, 0), size=(20, 20))
plantarch.enableSolidObstacleAvoidance(
obstacle_UUIDs=[ground],
avoidance_distance=0.05,
enable_fruit_adjustment=True
)
Troubleshooting
Problem: Plants still collide despite collision detection
- Check that
enableSoftCollisionAvoidance() was called before advanceTime()
- Increase
sample_count for better detection accuracy
- Reduce
inertia_weight for more responsive avoidance
- Verify collision-relevant organs are correctly configured
Problem: Poor performance with collision detection
- Mark static geometry with
setStaticObstacles()
- Reduce
sample_count (try 128 or 64)
- Reduce
view_half_angle_deg (try 60° or 50°)
- Filter organs to leaves only
- Use larger time steps in
advanceTime()
Problem: setStaticObstacles() fails
- Ensure
enableSoftCollisionAvoidance() is called FIRST
- Static obstacles require collision detection to be active
- C++ library enforces this requirement
Problem: Unnatural plant growth patterns
- Increase
inertia_weight for smoother growth (try 0.5-0.6)
- Reduce
look_ahead_distance to react to closer obstacles only
- Check that obstacle geometry is correctly positioned
Example Scripts
Complete working examples are available in docs/examples/:
plantarch_collision_sample.py: Comprehensive collision detection examples including:
- Basic soft collision avoidance
- Parameter tuning for different scenarios
- Hard obstacle avoidance with solid boundaries
- Performance optimization with static obstacles
- Organ-specific collision filtering
- Complete realistic workflow
Run the examples:
python docs/examples/plantarch_collision_sample.py
File I/O and Persistence
PlantArchitecture provides comprehensive file I/O capabilities to save and load plant structures, export geometry for external processing, and integrate with biomechanical analysis tools. These features enable plant structure persistence, library creation, and interoperability with other software.
Overview
Four file I/O methods are available:
writePlantStructureXML(): Save complete plant structure to XML for later loading
readPlantStructureXML(): Load saved plant structures from XML files
writePlantMeshVertices(): Export all mesh vertices for external processing
writeQSMCylinderFile(): Export to TreeQSM format for biomechanical analysis
All methods work with both string paths and pathlib.Path objects, and correctly handle relative/absolute paths.
Save and Load Plant Structures (XML)
XML format preserves complete plant architecture including shoot structure, organ properties, and growth state. This enables:
- Saving plant growth stages for later analysis
- Creating reusable plant libraries
- Sharing plant structures between simulations
- Checkpointing long-running simulations
Basic Usage:
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("bean")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=30.0)
plantarch.advanceTime(15.0)
plantarch.writePlantStructureXML(plant_id, "bean_day45.xml")
print("Plant saved successfully")
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("bean")
plant_ids = plantarch.readPlantStructureXML("bean_day45.xml")
print(f"Loaded {len(plant_ids)} plant(s)")
plantarch.advanceTime(10.0)
Important Notes:
- Plant model must be loaded (
loadPlantModelFromLibrary()) before calling readPlantStructureXML()
- XML files can contain multiple plants (returns list of plant IDs)
- Use
quiet=True parameter to suppress console output during loading
- XML format is Helios-native and preserves all plant structure details
Quiet Mode:
plant_ids = plantarch.readPlantStructureXML("bean_day45.xml", quiet=True)
Export Mesh Vertices
Export all vertex coordinates from plant geometry for external processing such as:
- Convex hull calculation
- Bounding volume computation
- Custom geometric analysis
- Integration with external modeling tools
Basic Usage:
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("tomato")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=25.0)
plantarch.writePlantMeshVertices(plant_id, "tomato_vertices.txt")
print("Vertices exported successfully")
with open("tomato_vertices.txt", 'r') as f:
for i, line in enumerate(f.readlines()[:5]):
x, y, z = line.strip().split()
print(f"Vertex {i+1}: ({x}, {y}, {z})")
Output Format:
- Plain text file with one vertex per line
- Three space-separated floating-point values per line:
x y z
- All vertices from all primitive meshes in the plant
- Coordinates in global coordinate system
Example Applications:
import numpy as np
vertices = np.loadtxt("tomato_vertices.txt")
min_coords = vertices.min(axis=0)
max_coords = vertices.max(axis=0)
print(f"Bounding box: {min_coords} to {max_coords}")
from scipy.spatial import ConvexHull
hull = ConvexHull(vertices)
volume = hull.volume
print(f"Convex hull volume: {volume:.3f} m³")
Export TreeQSM Cylinder Format
Export plant structure in TreeQSM (Quantitative Structure Model) format for biomechanical analysis and structural modeling. TreeQSM is widely used in forestry and biomechanics research.
Basic Usage:
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("almond")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=50.0)
plantarch.writeQSMCylinderFile(plant_id, "almond_qsm.txt")
print("TreeQSM file exported successfully")
TreeQSM Format Details:
The exported file contains tab-separated values with the following information for each cylinder:
- Cylinder dimensions (radius, length)
- Spatial position and orientation
- Branch topology (parent, extension, branch IDs)
- Branch hierarchy (order, position in branch)
- Quality metrics (distance, coverage)
Reference: Raumonen et al. (2013) "Fast Automatic Precision Tree Models from Terrestrial Laser Scanner Data" Remote Sensing 5(2):491-520
Use Cases:
- Biomechanical modeling and structural analysis
- Wind resistance calculations
- Carbon storage estimation
- Tree architecture studies
- Integration with TreeQSM analysis tools
Path Handling
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.
Using pathlib.Path:
from pathlib import Path
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("bean")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=20.0)
output_dir = Path("plant_data")
output_dir.mkdir(exist_ok=True)
vertices_file = output_dir / "vertices.txt"
plantarch.writePlantMeshVertices(plant_id, vertices_file)
xml_file = output_dir / "plant.xml"
plantarch.writePlantStructureXML(plant_id, xml_file)
qsm_file = output_dir / "qsm.txt"
plantarch.writeQSMCylinderFile(plant_id, qsm_file)
print(f"All files saved to {output_dir.absolute()}")
Path Features:
- Works with both relative and absolute paths
- User's working directory is preserved across all operations
- Automatic path resolution before C++ operations
- Compatible with
pathlib.Path and string paths
Creating Plant Libraries
Save plants at different growth stages to build reusable libraries:
from pathlib import Path
from pyhelios import Context, PlantArchitecture
library_dir = Path("soybean_library")
library_dir.mkdir(exist_ok=True)
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("soybean")
growth_stages = [10, 20, 30, 40, 50]
for age in growth_stages:
plant_id = plantarch.buildPlantInstanceFromLibrary(
base_position=vec3(0, 0, 0),
age=float(age)
)
filename = library_dir / f"soybean_day{age}.xml"
plantarch.writePlantStructureXML(plant_id, str(filename))
uuids = plantarch.getAllPlantUUIDs(plant_id)
print(f"Day {age}: {len(uuids)} primitives -> {filename.name}")
print(f"\nCreated library with {len(growth_stages)} growth stages")
print(f"Library location: {library_dir.absolute()}")
Using the Library:
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("soybean")
library_file = Path("soybean_library/soybean_day30.xml")
plant_ids = plantarch.readPlantStructureXML(str(library_file))
print(f"Loaded plant {plant_ids[0]} from library")
plantarch.advanceTime(10.0)
Multi-Plant Canopy Persistence
Save and load entire canopies for complex scene persistence:
from pathlib import Path
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("bean")
plant_ids = plantarch.buildPlantCanopyFromLibrary(
canopy_center=vec3(0, 0, 0),
plant_spacing=vec2(0.5, 0.5),
plant_count=int2(3, 3),
age=25.0
)
plantarch.advanceTime(15.0)
canopy_dir = Path("bean_canopy")
canopy_dir.mkdir(exist_ok=True)
for i, plant_id in enumerate(plant_ids):
filename = canopy_dir / f"plant_{i}.xml"
plantarch.writePlantStructureXML(plant_id, str(filename))
print(f"Saved {len(plant_ids)} plants to {canopy_dir}")
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("bean")
loaded_plants = []
for i in range(9):
filename = Path(f"bean_canopy/plant_{i}.xml")
plant_ids = plantarch.readPlantStructureXML(str(filename), quiet=True)
loaded_plants.extend(plant_ids)
print(f"Loaded {len(loaded_plants)} plants from canopy")
plantarch.advanceTime(10.0)
Integration Workflow Examples
Workflow 1: Growth Time Series
from pyhelios import Context, PlantArchitecture
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("maize")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=10.0)
time_series = [0, 5, 10, 15, 20]
for days in time_series:
if days > 0:
plantarch.advanceTime(float(days))
plantarch.writePlantStructureXML(plant_id, f"maize_t{days}.xml")
plantarch.writePlantMeshVertices(plant_id, f"maize_t{days}_vertices.txt")
print(f"Saved snapshot at t={days} days")
Workflow 2: External Analysis Pipeline
from pyhelios import Context, PlantArchitecture
import numpy as np
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("walnut")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=100.0)
plantarch.writeQSMCylinderFile(plant_id, "walnut_biomech.txt")
plantarch.writePlantMeshVertices(plant_id, "walnut_verts.txt")
plantarch.writePlantStructureXML(plant_id, "walnut_archive.xml")
print("Exported to multiple formats for external analysis")
vertices = np.loadtxt("walnut_verts.txt")
print(f"Loaded {len(vertices)} vertices for analysis")
Error Handling
File I/O operations include comprehensive error handling:
from pyhelios import Context, PlantArchitecture, PlantArchitectureError
with Context() as context:
with PlantArchitecture(context) as plantarch:
plantarch.loadPlantModelFromLibrary("bean")
plant_id = plantarch.buildPlantInstanceFromLibrary(vec3(0, 0, 0), age=20.0)
try:
plantarch.writePlantStructureXML(plant_id, "output/plant.xml")
except PlantArchitectureError as e:
print(f"Write failed: {e}")
try:
plant_ids = plantarch.readPlantStructureXML("nonexistent.xml")
except PlantArchitectureError as e:
print(f"Read failed: {e}")
try:
plantarch.writePlantStructureXML(-1, "plant.xml")
except ValueError as e:
print(f"Invalid parameter: {e}")
Common Errors:
ValueError: Invalid parameters (negative plant ID, empty filename)
PlantArchitectureError: File operation failed (permissions, missing file, invalid XML)
- Model not loaded before
readPlantStructureXML() (must call loadPlantModelFromLibrary() first)
Example Scripts
Complete working examples are available in docs/examples/:
plantarch_file_io_sample.py: Comprehensive file I/O examples including:
- Saving and loading plant structures
- Exporting mesh vertices for external processing
- TreeQSM format export for biomechanics
- Creating plant libraries with growth stages
- Multi-plant canopy persistence
- Flexible path handling with pathlib
Run the examples:
python docs/examples/plantarch_file_io_sample.py