Skip to content

Commit 2f09745

Browse files
committed
Add color mapping and fix make_n_trees.py
1 parent 99309ea commit 2f09745

File tree

10 files changed

+256
-134
lines changed

10 files changed

+256
-134
lines changed

__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""LPY Tree Simulation package."""
2+
3+
from .color_manager import ColorManager
4+
5+
__all__ = ["ColorManager"]

base_lpy.lpy

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,27 @@ from dataclasses import dataclass
2323
# Override these externs via Lsystem(..., variables) to point at a specific
2424
# architecture without editing this shared file.
2525
extern(
26+
color_manager = None,
2627
prototype_dict_path = "examples.Envy.Envy_prototypes.basicwood_prototypes",
2728
trunk_class_path = "examples.Envy.Envy_prototypes.Trunk",
2829
simulation_config_class_path = "examples.Envy.Envy_simulation.EnvySimulationConfig",
29-
point_generator_path = "examples.Envy.Envy_simulation.generate_points_v_trellis",
30-
energy_function_path = "examples.Envy.Envy_simulation.get_energy_mat",
31-
decide_function_path = "examples.Envy.Envy_simulation.decide_guide",
32-
tie_function_path = "examples.Envy.Envy_simulation.tie",
33-
prune_function_path = "examples.Envy.Envy_simulation.prune",
30+
simulation_class_path = "examples.Envy.Envy_simulation.EnvySimulation",
3431
axiom_pitch = 0.0,
3532
axiom_yaw = 0.0,
36-
)
37-
38-
# Resolve extern-provided dotted paths so the rest of the script can operate on
33+
)# Resolve extern-provided dotted paths so the rest of the script can operate on
3934
# concrete classes/functions exactly like the Envy/UFO drivers do.
4035
basicwood_prototypes = resolve_attr(prototype_dict_path)
4136
Trunk = resolve_attr(trunk_class_path)
4237
SimulationConfigClass = resolve_attr(simulation_config_class_path)
43-
point_generator_fn = resolve_attr(point_generator_path)
44-
get_energy_mat = resolve_attr(energy_function_path)
45-
decide_guide = resolve_attr(decide_function_path)
46-
tie = resolve_attr(tie_function_path)
47-
prune = resolve_attr(prune_function_path)
38+
SimulationClass = resolve_attr(simulation_class_path)
4839

4940
simulation_config = SimulationConfigClass()
41+
simulation = SimulationClass(simulation_config)
5042
main_trunk = Trunk(copy_from = basicwood_prototypes['trunk'])
5143

5244
# Build trellis geometry and cadence numbers up front so callbacks stay simple.
5345
trellis_support = Support(
54-
point_generator_fn(simulation_config),
46+
simulation.generate_points(),
5547
simulation_config.support_num_wires,
5648
simulation_config.support_spacing_wires,
5749
simulation_config.support_trunk_wire_point,
@@ -62,7 +54,30 @@ main_trunk.tying.guide_target = trellis_support.trunk_wire
6254

6355
# Track parent/child relationships for tying/pruning decisions.
6456
branch_hierarchy = {main_trunk.name: []}
65-
enable_color_labeling = simulation_config.label
57+
enable_semantic_labeling = simulation_config.semantic_label
58+
enable_instance_labeling = simulation_config.instance_label
59+
enable_per_cylinder_labeling = simulation_config.per_cylinder_label
60+
61+
if (enable_instance_labeling or enable_per_cylinder_labeling) and color_manager is None:
62+
raise ValueError(
63+
"Instance labeling is enabled but no color_manager extern was provided."
64+
)
65+
66+
67+
def _get_energy_mat(branches, arch, _config):
68+
return simulation.get_energy_mat(branches, arch)
69+
70+
71+
def _decide_guide(energy_matrix, branches, arch, _config):
72+
return simulation.decide_guide(energy_matrix, branches, arch)
73+
74+
75+
def _tie(lstring, _config):
76+
return simulation.tie(lstring)
77+
78+
79+
def _prune(lstring, _config):
80+
return simulation.prune(lstring)
6681

6782
# L-Py callbacks delegate to the Python helpers so architectures share logic.
6883
def StartEach(lstring):
@@ -81,12 +96,12 @@ def EndEach(lstring):
8196
simulation_config,
8297
main_trunk,
8398
getIterationNb,
84-
get_energy_mat,
85-
decide_guide,
86-
tie,
87-
prune,
99+
_get_energy_mat,
100+
_decide_guide,
101+
_tie,
102+
_prune,
88103
)
89-
104+
generalized_cylinder = getattr(simulation_config, "use_generalized_cylinder", False)
90105
# =============================================================================
91106
# L-SYSTEM GRAMMAR DEFINITION
92107
# =============================================================================
@@ -149,18 +164,28 @@ grow_object(plant_segment) :
149164
#Update internal state of the plant segment
150165
plant_segment.grow_one()
151166
#Update physical representation
152-
if enable_color_labeling:
167+
if enable_semantic_labeling:
153168
# Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment?
154169
r, g, b = plant_segment.info.color
170+
plant_part = plant_segment.name.split("_")[0] # Get the part type (e.g., "trunk", "branch")
171+
color_manager.set_unique_color((r,g,b), plant_part) # Ensure part type has a color assig
172+
nproduce SetColor(r,g,b)
173+
if enable_instance_labeling:
174+
# Instance-level labeling (unique color per branch instance)
175+
r, g, b = color_manager.get_unique_color(plant_segment.name)
155176
nproduce SetColor(r,g,b)
177+
156178
#Produce internode segments (n cylinders per growth step)
157179
n_cylinders = int(plant_segment.growth.growth_length / plant_segment.growth.cylinder_length)
158180
for i in range(n_cylinders):
181+
if enable_per_cylinder_labeling:
182+
r, g, b = color_manager.get_unique_color(plant_segment.name, if_exists=False)
183+
nproduce SetColor(r,g,b)
159184
nproduce I(plant_segment.growth.cylinder_length, plant_segment.growth.thickness, plant_segment)
160185
#Produce bud (after all cylinders in this growth step)
161186
if plant_segment.pre_bud_rule(plant_segment, simulation_config):
162187
for generated in plant_segment.post_bud_rule(plant_segment, simulation_config):
163-
nproduce new(generated[0], *generated[1])
188+
nproduce new(generated[0], *generated[1])
164189

165190
if should_bud(plant_segment, simulation_config):
166191
# Age-based bud production for lateral branching
@@ -190,15 +215,18 @@ bud(bud_parameters) :
190215
bud_parameters.num_buds+=1
191216
bud_parameters.type.info.num_branches+=1
192217

218+
nproduce [
219+
if generalized_cylinder:
220+
nproduce @Gc
193221
if hasattr(new_branch, 'curve_x_range'):
194222
# Curved branches: set up custom growth guide curve
195-
curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=time.time())
196-
nproduce[@GcSetGuide(curve, new_branch.growth.max_length)
197-
else:
198-
# Straight branches: use default orientation
199-
nproduce [@Gc
223+
curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=rd.randint(0,1000))
224+
nproduceSetGuide(curve, new_branch.growth.max_length)
200225
# Produce new branch with random orientation and growth object
201-
nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)@Ge]bud(bud_parameters)
226+
nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)
227+
if generalized_cylinder:
228+
nproduce @Ge
229+
produce ]bud(bud_parameters)
202230

203231
# -----------------------------------------------------------------------------
204232
# GEOMETRIC INTERPRETATION (HOMOMORPHISM)

color_manager.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Color management utilities for L-Py tree simulation system.
3+
4+
This module provides utilities for assigning unique colors to tree segments
5+
for visualization and labeling purposes.
6+
"""
7+
8+
import itertools
9+
import json
10+
11+
12+
class ColorManager:
13+
"""Manages assignment of unique colors to named entities."""
14+
15+
def __init__(self):
16+
self.color_to_name = {}
17+
self.name_to_color = {}
18+
# Permute all possible colors to make a list
19+
self.all_colors = list(itertools.product(range(256), repeat=3)) # 0-255 inclusive
20+
self.color_pointer = 0
21+
22+
def get_unique_color(self, name, if_exists=True):
23+
"""Get a unique RGB color tuple for the given name."""
24+
if name in self.name_to_color and if_exists:
25+
return self.name_to_color[name]
26+
27+
if self.color_pointer >= len(self.all_colors):
28+
raise ValueError("Ran out of unique colors!")
29+
30+
unique_color = self.all_colors[self.color_pointer]
31+
self.color_pointer += 1
32+
33+
# Assign and save mapping
34+
self.name_to_color[name] = unique_color
35+
self.color_to_name[unique_color] = name
36+
37+
return unique_color
38+
39+
def export_mapping(self, filename):
40+
"""Export color -> name mapping to JSON"""
41+
export_dict = {}
42+
for color, name in self.color_to_name.items():
43+
export_dict[str(color)] = name
44+
45+
with open(filename, "w") as f:
46+
json.dump(export_dict, f, indent=4)
47+
48+
print(f"Exported color mappings to {filename}")
49+
50+
def set_unique_color(self, color, name):
51+
"""Set a specific color for a given name."""
52+
self.name_to_color[name] = color
53+
self.color_to_name[color] = name

examples/Envy/Envy.lpy

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,25 @@ from pathlib import Path
22
from openalea.lpy import Lsystem
33
from openalea.lpy import *
44
from openalea.plantgl.all import *
5+
from lpy_treesim import ColorManager
56

67
BASE_LPY_PATH = Path(__file__).resolve().parents[2] / "base_lpy.lpy"
78

9+
color_manager = ColorManager()
10+
811
vars = {
912
"prototype_dict_path": "examples.Envy.Envy_prototypes.basicwood_prototypes",
1013
"trunk_class_path": "examples.Envy.Envy_prototypes.Trunk",
1114
"simulation_config_class_path": "examples.Envy.Envy_simulation.EnvySimulationConfig",
12-
"point_generator_path": "examples.Envy.Envy_simulation.generate_points_v_trellis",
13-
"energy_function_path": "examples.Envy.Envy_simulation.get_energy_mat",
14-
"decide_function_path": "examples.Envy.Envy_simulation.decide_guide",
15-
"tie_function_path": "examples.Envy.Envy_simulation.tie",
16-
"prune_function_path": "examples.Envy.Envy_simulation.prune",
15+
"simulation_class_path": "examples.Envy.Envy_simulation.EnvySimulation",
16+
"color_manager": color_manager,
1717
"axiom_pitch": 0.0,
1818
"axiom_yaw": 0.0,
1919
}
2020

2121
lsystem = Lsystem(str(BASE_LPY_PATH), vars)
2222

2323
for lstring in lsystem:
24-
lsystem.plot(lstring)
24+
lsystem.plot(lstring)
25+
26+
# color_manager.export_mapping("envy_color_mapping.json")

examples/Envy/Envy_simulation.py

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ class EnvySimulationConfig(SimulationConfig):
2525
trellis_z_spacing: float = 0.45
2626

2727
# Envy-specific Growth Parameters
28-
growth_length: float = 0.1
29-
bud_spacing_age: int = 2
28+
semantic_label: bool = True
29+
instance_label: bool = False
30+
per_cylinder_label: bool = False
3031

3132

3233
class EnvySimulation(TreeSimulationBase):
@@ -60,31 +61,4 @@ def generate_points(self):
6061
pts.append((x[i], y[i], z[i]))
6162
return pts
6263

63-
64-
# Backwards compatibility: provide standalone functions that use the class
65-
def generate_points_v_trellis(simulation_config):
66-
"""Backward compatibility wrapper for generate_points."""
67-
sim = EnvySimulation(simulation_config)
68-
return sim.generate_points()
69-
70-
def get_energy_mat(branches, arch, simulation_config):
71-
"""Backward compatibility wrapper for get_energy_mat."""
72-
sim = EnvySimulation(simulation_config)
73-
return sim.get_energy_mat(branches, arch)
74-
75-
def decide_guide(energy_matrix, branches, arch, simulation_config):
76-
"""Backward compatibility wrapper for decide_guide."""
77-
sim = EnvySimulation(simulation_config)
78-
return sim.decide_guide(energy_matrix, branches, arch)
79-
80-
def prune(lstring, simulation_config):
81-
"""Backward compatibility wrapper for prune."""
82-
sim = EnvySimulation(simulation_config)
83-
return sim.prune(lstring)
84-
85-
def tie(lstring, simulation_config):
86-
"""Backward compatibility wrapper for tie."""
87-
sim = EnvySimulation(simulation_config)
88-
return sim.tie(lstring)
89-
9064

examples/UFO/UFO.lpy

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ from pathlib import Path
22
from openalea.lpy import Lsystem
33
from openalea.lpy import *
44
from openalea.plantgl.all import *
5+
from lpy_treesim import ColorManager
56

67
BASE_LPY_PATH = Path(__file__).resolve().parents[2] / "base_lpy.lpy"
78

9+
color_manager = ColorManager()
10+
811
vars = {
912
"prototype_dict_path": "examples.UFO.UFO_prototypes.basicwood_prototypes",
1013
"trunk_class_path": "examples.UFO.UFO_prototypes.Trunk",
1114
"simulation_config_class_path": "examples.UFO.UFO_simulation.UFOSimulationConfig",
12-
"point_generator_path": "examples.UFO.UFO_simulation.generate_points_ufo",
13-
"energy_function_path": "examples.UFO.UFO_simulation.get_energy_mat",
14-
"decide_function_path": "examples.UFO.UFO_simulation.decide_guide",
15-
"tie_function_path": "examples.UFO.UFO_simulation.tie",
16-
"prune_function_path": "examples.UFO.UFO_simulation.prune",
15+
"simulation_class_path": "examples.UFO.UFO_simulation.UFOSimulation",
16+
"color_manager": color_manager,
1717
"axiom_pitch": 270.0,
1818
"axiom_yaw": 0.0,
1919
}
@@ -22,3 +22,5 @@ lsystem = Lsystem(str(BASE_LPY_PATH), vars)
2222

2323
for lstring in lsystem:
2424
lsystem.plot(lstring)
25+
26+
# color_manager.export_mapping("ufo_color_mapping.json")

examples/UFO/UFO_simulation.py

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class UFOSimulationConfig(SimulationConfig):
2626

2727
# UFO-specific Growth Parameters
2828
thickness_multiplier: float = 1.2 # Multiplier for internode thickness
29+
semantic_label: bool = True
30+
instance_label: bool = False
31+
per_cylinder_label: bool = False
2932

3033

3134
class UFOSimulation(TreeSimulationBase):
@@ -61,31 +64,3 @@ def generate_points(self):
6164
wire_attachment_points.append((x[point_index], y[point_index], z[point_index]))
6265

6366
return wire_attachment_points
64-
65-
66-
# Backwards compatibility: provide standalone functions that use the class
67-
# Backwards compatibility: provide standalone functions that use the class
68-
def generate_points_ufo(simulation_config):
69-
"""Backward compatibility wrapper for generate_points."""
70-
sim = UFOSimulation(simulation_config)
71-
return sim.generate_points()
72-
73-
def get_energy_mat(branches, arch, simulation_config):
74-
"""Backward compatibility wrapper for get_energy_mat."""
75-
sim = UFOSimulation(simulation_config)
76-
return sim.get_energy_mat(branches, arch)
77-
78-
def decide_guide(energy_matrix, branches, arch, simulation_config):
79-
"""Backward compatibility wrapper for decide_guide."""
80-
sim = UFOSimulation(simulation_config)
81-
return sim.decide_guide(energy_matrix, branches, arch)
82-
83-
def prune(lstring, simulation_config):
84-
"""Backward compatibility wrapper for prune."""
85-
sim = UFOSimulation(simulation_config)
86-
return sim.prune(lstring)
87-
88-
def tie(lstring, simulation_config):
89-
"""Backward compatibility wrapper for tie."""
90-
sim = UFOSimulation(simulation_config)
91-
return sim.tie(lstring)

0 commit comments

Comments
 (0)