diff --git a/.gitignore b/.gitignore index 592e1f4..fa570f4 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,6 @@ cython_debug/ dataset*/ .DS_Store +GEMINI.md +.vscode/ +__* \ No newline at end of file diff --git a/__init__.py b/__init__.py index e69de29..ca96213 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,5 @@ +"""LPY Tree Simulation package.""" + +from .color_manager import ColorManager + +__all__ = ["ColorManager"] diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 1495097..0000000 Binary files a/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/__init__.cpython-38.pyc b/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 2b97dab..0000000 Binary files a/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/__pycache__/__init__.cpython-39.pyc b/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 08db88c..0000000 Binary files a/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/helper.cpython-312.pyc b/__pycache__/helper.cpython-312.pyc deleted file mode 100644 index bae7b12..0000000 Binary files a/__pycache__/helper.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/helper.cpython-38.pyc b/__pycache__/helper.cpython-38.pyc deleted file mode 100644 index 5577cb9..0000000 Binary files a/__pycache__/helper.cpython-38.pyc and /dev/null differ diff --git a/__pycache__/helper.cpython-39.pyc b/__pycache__/helper.cpython-39.pyc deleted file mode 100644 index 40c8eea..0000000 Binary files a/__pycache__/helper.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/stochastic_tree.cpython-312.pyc b/__pycache__/stochastic_tree.cpython-312.pyc deleted file mode 100644 index 2c0d5d7..0000000 Binary files a/__pycache__/stochastic_tree.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/stochastic_tree.cpython-38.pyc b/__pycache__/stochastic_tree.cpython-38.pyc deleted file mode 100644 index e5d2fe1..0000000 Binary files a/__pycache__/stochastic_tree.cpython-38.pyc and /dev/null differ diff --git a/__pycache__/stochastic_tree.cpython-39.pyc b/__pycache__/stochastic_tree.cpython-39.pyc deleted file mode 100644 index 617614f..0000000 Binary files a/__pycache__/stochastic_tree.cpython-39.pyc and /dev/null differ diff --git a/base_lpy.lpy b/base_lpy.lpy new file mode 100644 index 0000000..51a226d --- /dev/null +++ b/base_lpy.lpy @@ -0,0 +1,253 @@ +""" +Shared L-System driver for Envy, UFO, and future trellis architectures. +Configure via extern variables passed to Lsystem(..., variables). +""" +import os +import sys +import time +import numpy as np +import random as rd + +# Ensure repository root is importable regardless of working directory +CURRENT_DIR = os.path.dirname(__file__) +if CURRENT_DIR not in sys.path: + sys.path.append(CURRENT_DIR) + +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +from helper import * +from dataclasses import dataclass + +# ----------------------------------------------------------------------------- +# EXTERNALLY CONFIGURABLE PATHS +# ----------------------------------------------------------------------------- +# Override these externs via Lsystem(..., variables) to point at a specific +# architecture without editing this shared file. +extern( + color_manager = None, + prototype_dict_path = "examples.Envy.Envy_prototypes.basicwood_prototypes", + trunk_class_path = "examples.Envy.Envy_prototypes.Trunk", + simulation_config_class_path = "examples.Envy.Envy_simulation.EnvySimulationConfig", + simulation_class_path = "examples.Envy.Envy_simulation.EnvySimulation", + axiom_pitch = 0.0, + axiom_yaw = 0.0, +)# Resolve extern-provided dotted paths so the rest of the script can operate on +# concrete classes/functions exactly like the Envy/UFO drivers do. +basicwood_prototypes = resolve_attr(prototype_dict_path) +Trunk = resolve_attr(trunk_class_path) +SimulationConfigClass = resolve_attr(simulation_config_class_path) +SimulationClass = resolve_attr(simulation_class_path) + +simulation_config = SimulationConfigClass() +simulation = SimulationClass(simulation_config) +main_trunk = Trunk(copy_from = basicwood_prototypes['trunk']) + +# Build trellis geometry and cadence numbers up front so callbacks stay simple. +trellis_support = Support( + simulation.generate_points(), + simulation_config.support_num_wires, + simulation_config.support_spacing_wires, + simulation_config.support_trunk_wire_point, +) +tying_interval_iterations = simulation_config.num_iteration_tie +pruning_interval_iterations = simulation_config.num_iteration_prune +main_trunk.tying.guide_target = trellis_support.trunk_wire + +# Track parent/child relationships for tying/pruning decisions. +branch_hierarchy = {main_trunk.name: []} +enable_semantic_labeling = simulation_config.semantic_label +enable_instance_labeling = simulation_config.instance_label +enable_per_cylinder_labeling = simulation_config.per_cylinder_label + +if (enable_instance_labeling or enable_per_cylinder_labeling) and color_manager is None: + raise ValueError( + "Instance labeling is enabled but no color_manager extern was provided." + ) + + +def _get_energy_mat(branches, arch, _config): + return simulation.get_energy_mat(branches, arch) + + +def _decide_guide(energy_matrix, branches, arch, _config): + return simulation.decide_guide(energy_matrix, branches, arch) + + +def _tie(lstring, _config): + return simulation.tie(lstring) + + +def _prune(lstring, _config): + return simulation.prune(lstring) + +# L-Py callbacks delegate to the Python helpers so architectures share logic. +def StartEach(lstring): + """Proxy to shared tying preparation logic.""" + start_each_common(lstring, branch_hierarchy, trellis_support, main_trunk) + + +def EndEach(lstring): + """Proxy to shared tying/pruning orchestration logic.""" + return end_each_common( + lstring, + branch_hierarchy, + trellis_support, + tying_interval_iterations, + pruning_interval_iterations, + simulation_config, + main_trunk, + getIterationNb, + _get_energy_mat, + _decide_guide, + _tie, + _prune, + ) +generalized_cylinder = getattr(simulation_config, "use_generalized_cylinder", False) +# ============================================================================= +# L-SYSTEM GRAMMAR DEFINITION +# ============================================================================= +# This section defines the formal grammar for the tree growth simulation. +# The L-System uses modules (symbols) to represent different plant components +# and their growth behaviors. + +# ----------------------------------------------------------------------------- +# MODULE DECLARATIONS +# ----------------------------------------------------------------------------- +# Define the vocabulary of symbols used in the L-System grammar. +# Each module represents a different type of plant component or operation. +module Attractors # Trellis support structure that guides branch growth +module grow_object # Growing plant parts (trunk, branches, spurs) with length/thickness +module bud # Dormant buds that can break to produce new branches +module branch # Branch segments in the L-System string +module WoodStart # Starting point of wood segments (used for tying operations) + +# ----------------------------------------------------------------------------- +# GLOBAL L-SYSTEM PARAMETERS +# ----------------------------------------------------------------------------- +# Create a growth guide curve for the initial trunk development +trunk_guide_curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_value=time.time()) + +# ----------------------------------------------------------------------------- +# AXIOM (STARTING STRING) +# ----------------------------------------------------------------------------- +# The initial L-System string that begins the simulation. +# Starts with the trellis attractors, sets up the trunk guide curve, +# and initializes the trunk growth with proper orientation. +Axiom: Attractors(trellis_support)SetGuide(trunk_guide_curve, main_trunk.growth.max_length)[@GcGetPos(main_trunk.location.start)WoodStart(ParameterSet(type = main_trunk))&(axiom_pitch)/(axiom_yaw)grow_object(main_trunk)GetPos(main_trunk.location.end)@Ge] + +# ----------------------------------------------------------------------------- +# DERIVATION PARAMETERS +# ----------------------------------------------------------------------------- +# Set the maximum number of derivation steps for the L-System +derivation length: simulation_config.derivation_length + +# ----------------------------------------------------------------------------- +# PRODUCTION RULES +# ----------------------------------------------------------------------------- +# Define how each module type evolves during each derivation step. +# These rules control the growth, branching, and development of the tree. + +production: + +# GROW_OBJECT PRODUCTION RULE +# Handles the growth of plant segments (trunk, branches, spurs) +# Determines whether to continue growing, stop, or produce buds +grow_object(plant_segment) : + if plant_segment == None: + # Null object - terminate this branch + produce * + if plant_segment.length >= plant_segment.growth.max_length: + # Maximum length reached - stop growing this segment + nproduce * + else: + # Continue growing - update segment properties + nproduce SetContour(plant_segment.contour) + #Update internal state of the plant segment + plant_segment.grow_one() + #Update physical representation + if enable_semantic_labeling: + # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? + r, g, b = plant_segment.info.color + plant_part = plant_segment.name.split("_")[0] # Get the part type (e.g., "trunk", "branch") + color_manager.set_unique_color((r,g,b), plant_part) # Ensure part type has a color assig + nproduce SetColor(r,g,b) + if enable_instance_labeling: + # Instance-level labeling (unique color per branch instance) + r, g, b = color_manager.get_unique_color(plant_segment.name) + nproduce SetColor(r,g,b) + + #Produce internode segments (n cylinders per growth step) + n_cylinders = int(plant_segment.growth.growth_length / plant_segment.growth.cylinder_length) + for i in range(n_cylinders): + if enable_per_cylinder_labeling: + r, g, b = color_manager.get_unique_color(plant_segment.name, if_exists=False) + nproduce SetColor(r,g,b) + nproduce I(plant_segment.growth.cylinder_length, plant_segment.growth.thickness, plant_segment) + #Produce bud (after all cylinders in this growth step) + if plant_segment.pre_bud_rule(plant_segment, simulation_config): + for generated in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(generated[0], *generated[1]) + + if should_bud(plant_segment, simulation_config): + # Age-based bud production for lateral branching + nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) + + if plant_segment.post_bud_rule(plant_segment, simulation_config): + for generated in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(generated[0], *generated[1]) + + produce grow_object(plant_segment) + +# BUD PRODUCTION RULE +# Controls bud break and branch initiation +# Determines when buds activate to produce new branches +bud(bud_parameters) : + if bud_parameters.type.is_bud_break(bud_parameters.num_buds): + # Bud break condition met - create new branch + + new_branch = bud_parameters.type.create_branch() + if new_branch == None: + # Branch creation failed - terminate + produce * + # Register new branch in parent-child relationship tracking + branch_hierarchy[new_branch.name] = [] + branch_hierarchy[bud_parameters.type.name].append(new_branch) + # Update branch counters + bud_parameters.num_buds+=1 + bud_parameters.type.info.num_branches+=1 + + nproduce [ + if generalized_cylinder: + nproduce @Gc + if hasattr(new_branch, 'curve_x_range'): + # Curved branches: set up custom growth guide curve + 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)) + nproduceSetGuide(curve, new_branch.growth.max_length) + # Produce new branch with random orientation and growth object + 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) + if generalized_cylinder: + nproduce @Ge + produce ]bud(bud_parameters) + +# ----------------------------------------------------------------------------- +# GEOMETRIC INTERPRETATION (HOMOMORPHISM) +# ----------------------------------------------------------------------------- +# Map abstract L-System modules to concrete 3D geometry for rendering. +# These rules define how the symbolic representation becomes visual. +homomorphism: + +# Internode segments become cylinders with length and radius +I(a,r,o) --> F(a,r) +# Branch segments also become cylinders +S(a,r,o) --> F(a,r) + +# ----------------------------------------------------------------------------- +# ATTRACTOR VISUALIZATION +# ----------------------------------------------------------------------------- +# Additional production rules for displaying trellis attractor points +production: +Attractors(trellis_support): + # Display enabled attractor points as visual markers + points_to_display = trellis_support.attractor_grid.get_enabled_points() + if len(points_to_display) > 0: + produce [,(3) @g(PointSet(points_to_display,width=simulation_config.attractor_point_width))] + diff --git a/bug_test.lpy b/bug_test.lpy deleted file mode 100644 index ff30ad8..0000000 --- a/bug_test.lpy +++ /dev/null @@ -1,307 +0,0 @@ -""" -Tying, Pruning, and Labelling Envy architecture tree -""" -import sys -sys.path.append('../') -from stochastic_tree import Support, BasicWood -import numpy as np -import random as rd -import copy -import gc -import time - -from helper import * - -class Spur(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: list = [0,1,1], order: int = 1, prototype_dict: dict = {}, name = None, color = None): - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Spur.count+=1 - - def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.1) - - def create_branch(self): - return None - - def grow(self): - pass - -class Branch(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Branch.count+=1 - - def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) - - def create_branch(self): - new_ob = Spur(copy_from = self.prototype_dict['spur']) - return new_ob - - def grow(self): - pass - -class NonTrunk(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Branch.count+=1 - - def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) - - def create_branch(self): - new_ob = Spur(copy_from = self.prototype_dict['spur']) - return new_ob - - def grow(self): - pass - - -class Trunk(BasicWood): - count = 0 - """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 0, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Trunk.count+=1 - - def is_bud_break(self, num_buds_segment): - if (rd.random() > 0.02*(1 - num_buds_segment/self.max_buds_segment)): - return False - return True - - def create_branch(self): - if rd.random() > 0.8: - return Spur(copy_from = self.prototype_dict['spur']) - else: - return Branch(copy_from = self.prototype_dict['branch']) - - def grow(self): - pass - - - - -#Pass transition probabs? --> solve with abstract classes - -#basicwood_prototypes = {} -#basicwood_prototypes['trunk'] = Trunk(tie_axis = [0,1,1], max_length = 20, thickness_increment = 0.02, prototype_dict = basicwood_prototypes, color = 0) -#basicwood_prototypes['branch'] = Branch(tie_axis = [0,1,1], max_length = 20, thickness_increment = 0.005, prototype_dict = basicwood_prototypes, color = 1) -#basicwood_prototypes['spur'] = Spur(tie_axis = [0,1,1], max_length = 1, thickness_increment = 0.005, prototype_dict = basicwood_prototypes, color = 2) - -growth_length = 0.1 -#everything is relative to growth length -basicwood_prototypes = {} -basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 2.5/growth_length, thickness = 0.01, growth_length = 0.1,thickness_increment = 0.0005, prototype_dict = basicwood_prototypes, color = 0) -basicwood_prototypes['branch'] = Branch(tie_axis = (0,1,1), max_length = .45/growth_length, thickness = 0.01, growth_length = 0.1,thickness_increment = 0.00005, prototype_dict = basicwood_prototypes, color = 1) -basicwood_prototypes['nontrunkbranch'] = NonTrunk(tie_axis = (0,0,1), max_length = 0.1/growth_length, growth_length = 0.1, thickness = 0.0001,thickness_increment = 0.0001, prototype_dict = basicwood_prototypes, color = 1) -basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.01/growth_length, thickness = 0.005, growth_length = 0.01,thickness_increment = 0., prototype_dict = basicwood_prototypes, color = 2) - -#init -trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) -time_count = 0 -label = True -def generate_points_v_trellis(): - x = np.full((7,), 1.45).astype(float) - #z = np.arange(3, 24, 3).astype(float) - y = np.full((7,), 0).astype(float) - z = np.arange(0.6,3.4, 0.45) - pts = [] - id = 0 - for i in range(x.shape[0]): - pts.append((-x[i], y[i], z[i])) - id+=1 - pts.append((x[i], y[i], z[i])) - id+=1 - return pts - - - -support = Support(generate_points_v_trellis(), 14 , 1 , None, (0,0,1), None) -num_iteration_tie = 30 - -###Tying stuff begins - -def ed(a,b): - return np.linalg.norm(a-b) - -def get_energy_mat(branches, arch): - #branches = [i for i in branches if "Branch" in i.name] - num_branches = len(branches) - num_wires = len(list(arch.branch_supports.values())) - energy_matrix = np.ones((num_branches,num_wires))*np.inf - #print(energy_matrix.shape) - for branch_id, branch in enumerate(branches): - if branch.has_tied: - continue - for wire_id, wire in arch.branch_supports.items(): - if wire.num_branch>=1: - continue - energy_matrix[branch_id][wire_id] = ed(wire.point,branch.end)/2+ed(wire.point,branch.start)/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age) - return energy_matrix - -def decide_guide(energy_matrix, branches, arch): - for i in range(energy_matrix.shape[0]): - min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) - #print(min_arg) - if(energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf):# or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 1: - return - if not (branches[min_arg[0][0]].has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): - #print("Imp:",min_arg[0][0], min_arg[0][1], energy_matrix[min_arg[0][0]][min_arg[0][1]]) - branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) - #trellis_wires.trellis_pts[min_arg[0][1]].num_branches+=1 - for j in range(energy_matrix.shape[1]): - energy_matrix[min_arg[0][0]][j] = np.inf - for j in range(energy_matrix.shape[0]): - energy_matrix[j][min_arg[0][1]] = np.inf - -def tie(lstring): - for j,i in enumerate(lstring): - if i == 'C' and i[0].type.__class__.__name__ == 'Branch': - if i[0].type.tie_updated == False: - continue - curr = i[0] - if i[0].type.guide_points: - #print("tying ", i[0].type.name, i[0].type.guide_target.point) - i[0].type.tie_updated = False - i[0].type.guide_target.add_branch() - lstring, count = i[0].type.tie_lstring(lstring, j) - - return True - return False - -#Pruning strategy - -def pruning_strategy(lstring): #Remove remnants of cut - cut = False - - for j,i in enumerate(lstring): - - if i.name == 'C' and i[0].type.age > 10 and i[0].type.has_tied == False and i[0].type.cut == False: - - i[0].type.cut = True - #print("Cutting", i[0].type.name) - lstring = cut_from(j, lstring) - - return True - - return False - -def StartEach(lstring): - global parent_child_dict - for i in parent_child_dict[trunk_base.name]: - if i.tie_updated == False: - i.tie_update() - - -def EndEach(lstring): - global parent_child_dict, support - tied = False - - if (getIterationNb()+1)%num_iteration_tie == 0: - energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) - print(energy_matrix) - decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) - for branch in parent_child_dict[trunk_base.name]: - branch.update_guide(branch.guide_target) - print(branch.name, branch.guide_target) - while tie(lstring): - pass - while pruning_strategy(lstring): - pass - return lstring - -parent_child_dict = {} -parent_child_dict[trunk_base.name] = [] -#print(generate_points_ufo()) -module Attractors -module grow_object -module bud -module branch -module C -Axiom: Attractors(support)grow_object(trunk_base) -derivation length: 100 - -production: -#Decide whether branch internode vs trunk internode need to be the same size. -grow_object(o) : - if o == None: - produce * - if o.length >= o.max_length: - o.age+=1 - nproduce * - else: - if label: - nproduce SetColor(o.color) - o.grow_one() - produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))grow_object(o) - -bud(t) : - if t.type.is_bud_break(t.num_buds): - new_object = t.type.create_branch() - if new_object == None: - produce * - parent_child_dict[new_object.name] = [] - parent_child_dict[t.type.name].append(new_object) - #Store new object somewhere - t.num_buds+=1 - t.type.num_branches+=1 - nproduce [@RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*90)grow_object(new_object)GetPos(new_object.end)]bud(t) - - -I(s,r,o) --> I(s,r+o.thickness_increment, o) -_(r) --> _(r+o.thickness_increment) - -homomorphism: - -I(a,r,o) --> F(a,r) -S(a,r,o) --> F(a,r) - -production: -Attractors(support): - pttodisplay = support.attractor_grid.get_enabled_points() - if len(pttodisplay) > 0: - produce [,(3) @g(PointSet(pttodisplay,width=10))] diff --git a/color_manager.py b/color_manager.py new file mode 100644 index 0000000..6662653 --- /dev/null +++ b/color_manager.py @@ -0,0 +1,53 @@ +""" +Color management utilities for L-Py tree simulation system. + +This module provides utilities for assigning unique colors to tree segments +for visualization and labeling purposes. +""" + +import itertools +import json + + +class ColorManager: + """Manages assignment of unique colors to named entities.""" + + def __init__(self): + self.color_to_name = {} + self.name_to_color = {} + # Permute all possible colors to make a list + self.all_colors = list(itertools.product(range(256), repeat=3)) # 0-255 inclusive + self.color_pointer = 0 + + def get_unique_color(self, name, if_exists=True): + """Get a unique RGB color tuple for the given name.""" + if name in self.name_to_color and if_exists: + return self.name_to_color[name] + + if self.color_pointer >= len(self.all_colors): + raise ValueError("Ran out of unique colors!") + + unique_color = self.all_colors[self.color_pointer] + self.color_pointer += 1 + + # Assign and save mapping + self.name_to_color[name] = unique_color + self.color_to_name[unique_color] = name + + return unique_color + + def export_mapping(self, filename): + """Export color -> name mapping to JSON""" + export_dict = {} + for color, name in self.color_to_name.items(): + export_dict[str(color)] = name + + with open(filename, "w") as f: + json.dump(export_dict, f, indent=4) + + print(f"Exported color mappings to {filename}") + + def set_unique_color(self, color, name): + """Set a specific color for a given name.""" + self.name_to_color[name] = color + self.color_to_name[color] = name \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..26aeec1 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,26 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) + +set BUILDDIR=_build +set SOURCEDIR=source + + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 "%SOURCEDIR%" "%BUILDDIR%" %SPHINXOPTS% %O% + +popd +goto end + +:help +%SPHINXBUILD% -M help "%SOURCEDIR%" "%BUILDDIR%" %SPHINXOPTS% %O% + +popd +:end diff --git a/docs/source/conf.py b/docs/source/conf.py index 602737b..9516d38 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,6 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/source/files.rst b/docs/source/files.rst index 09e5bd1..48468f0 100644 --- a/docs/source/files.rst +++ b/docs/source/files.rst @@ -1,17 +1,74 @@ -========= -Files -========= +Files and Directory Structure +============================= -This page gives a brief info of all the files provided in treesim_lpy repository +The project is organized so that every custom tree lives wholly inside the +`examples/` package, while the shared runtime sits at the repository root. -* examples/* - * examples/static_envy.lpy - Grows a tree in the envy architecture following predefined rules. No thinning and/or tying takes place. - * examples/static_ufo.lpy - Grows a tree in the ufo architecture following predefined rules. No thinning and/or tying takes place. - * examples/UFO_tie_prune_label.lpy - This file allows the growth of a tree in ufo architecture following the given thinning and tying rules. Further has the option to label different segments as well - * examples/Envy_tie_prune_label.lpy - This file allows the growth of a tree in ufo architecture following the given thinning and tying rules. Further has the option to label different segments as well +Top-level Python modules +------------------------ -* modules_test/* - * All files in this folder use classes/functions defined in stochastic_tree.py. They can be a good example on how to use the BasicWood, Wire and Support classes +``base_lpy.lpy`` + The canonical L-Py grammar. It loads extern variables that point to your + prototype dictionary, simulation classes, and color manager, then drives the + derivation/prune/tie loop. -* other_files/* - * These files may or may not work. These were used in previous iterations of treesim_lpy. Kept to be used as a reference. \ No newline at end of file +``simulation_base.py`` + Defines `SimulationConfig` and `TreeSimulationBase`. All simulation files in + `examples/` inherit from these classes. + +``stochastic_tree.py`` + Hosts the `TreeBranch`, `BasicWood`, `Support`, and related data structures + referenced by prototypes. + +``color_manager.py`` + Implements `ColorManager`, which tracks per-instance color IDs and can dump + them via `export_mapping` to JSON. + +Documentation (`docs/`) +----------------------- + +Sphinx project containing the pages you are reading now. Run `make html` inside +`lpy_treesim/docs` to build the site locally. + +Examples (`examples/`) +---------------------- + +Each subfolder describes one tree training system. For `UFO` you will find: + +``examples/UFO/UFO_prototypes.py`` + Prototype classes (`Trunk`, `Branch`, `Spur`, etc.) plus the + `basicwood_prototypes` dictionary. + +``examples/UFO/UFO_simulation.py`` + `UFOSimulationConfig` and `UFOSimulation`, consumed by the base grammar. + +``examples/UFO/UFO.lpy`` + Optional GUI entry point if you want to run the species manually inside + L-Py. Most development happens via `base_lpy.lpy`, but the standalone files + are helpful for debugging. + +Add a new tree type by duplicating this directory and renaming files to match +your `--tree_name` argument. + +Tree generation utilities (`tree_generation/`) +--------------------------------------------- + +``tree_generation/make_n_trees.py`` + CLI entry point that imports the base grammar, wires in your prototypes, and + exports `.ply` meshes plus `{...}_colors.json` label maps. Accepts + `--num_trees`, `--namespace`, `--rng-seed`, and `--output_dir`. + +``tree_generation/helpers.py`` + Contains `write` (PLY serializer) and other small utilities referenced by + the generator. + +Supporting assets +----------------- + +``dataset/`` + Destination for generated `.ply` files by default. Subfolders such as + `test_output/` are safe to delete or replace with your own datasets. + +``media/`` and ``other_files/`` + Legacy L-Py grammars, renderings, and experiments kept for historical + reference. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 6a1015a..445697d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,25 +1,35 @@ +.. lpy_treesim documentation master file, created by + sphinx-quickstart on Tue Jul 16 11:58:11 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. -TreeSim_Lpy documentation -=================================== +Welcome to lpy_treesim's documentation! +======================================= -TreeSim_Lpy is a tree growing simulator based upon L-py (Fred Boudon). Using TreeSim you can perform various tasks -like growing/pruning and tying trees as different architectures, with examples for a UFO cherry and Envy apple provided. -Check out the :doc:`usage` section for further information, including -how to :doc:`installation` the project. +`lpy_treesim` bundles a reusable L-Py grammar (`base_lpy.lpy`), +prototype definitions, and automation scripts so you can describe a new tree +architecture and batch-generate thousands of 3D assets. These docs walk +through the entire workflow: -.. note:: - - This project is under active development. - -Contents --------- +* set up the environment and dependencies, +* design prototypes plus simulation parameters, +* run the `make_n_trees.py` generator with deterministic naming, and +* understand how the supporting modules fit together. .. toctree:: + :maxdepth: 2 + :caption: Contents: - Home installation usage files resources methods - + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/installation.rst b/docs/source/installation.rst index c043691..d2bca7e 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,45 +1,63 @@ Installation -============== +============ -Installing L-Py -*************** +`lpy_treesim` ships as a Python package plus a collection of L-Py grammars, so +you need both the OpenAlea/L-Py toolchain and the Python modules in this repo. -``L-Py`` distribution is based on the ``conda`` software environment management system. -To install conda, you may refer to its installation page: https://docs.conda.io/projects/conda/en/latest/user-guide/install/ +Prerequisites +------------- +- **Conda (recommended)** for installing `openalea.lpy` and PlantGL. +- **Python 3.9+** for running the helper scripts. +- A GPU is not required; everything runs on CPU. -Installing binaries using conda +Set up the L-Py environment +--------------------------- +1. Create a dedicated environment that contains L-Py and PlantGL: + .. code-block:: bash -To install L-Py, you need to create an environment (named lpy in this case) : + conda create -n lpy openalea.lpy plantgl python=3.9 -c fredboudon -c conda-forge -.. code-block:: bash +2. Activate the environment any time you work on the project: - conda create -n lpy openalea.lpy -c fredboudon -c conda-forge + .. code-block:: bash -The package is retrieved from the ``fredboudon`` channel (developement) and its dependencies will be taken from ``conda-forge`` channel. + conda activate lpy -Then, you need to activate the L-Py environment +3. Validate the installation by launching the GUI (optional but handy for + debugging grammars): -.. code-block:: bash + .. code-block:: bash - conda activate lpy + lpy -And then run L-Py +Install `lpy_treesim` +--------------------- -.. code-block:: bash +With the environment active, clone and install the package in editable mode so +that L-Py can import your custom prototypes: - lpy +.. code-block:: bash -For any issues with L-py, please check the documentation of L-Py provided here https://lpy.readthedocs.io/en/latest/user/installing.html + git clone https://github.com/OSUrobotics/lpy_treesim.git + cd lpy_treesim + pip install -e . +Editable installs expose modules such as `lpy_treesim.ColorManager` and ensure +`examples/` can import the shared base grammar. -Installing TreeSim_Lpy -*********************** +Optional tooling +----------------- -With the conda environment for L-Py set, next we need to clone the TreeSim_Lpy repository. To do that run +The repository includes a Sphinx documentation project. To build the docs +locally install Sphinx, then run `make`: .. code-block:: bash - git clone https://github.com/OSUrobotics/treesim_lpy.git \ No newline at end of file + cd lpy_treesim/lpy_treesim/docs + pip install sphinx + make html + +Open `_build/html/index.html` in a browser to preview the rendered docs. \ No newline at end of file diff --git a/docs/source/methods.rst b/docs/source/methods.rst index 5d02cde..c6503f8 100644 --- a/docs/source/methods.rst +++ b/docs/source/methods.rst @@ -1,2 +1,74 @@ -.. automodule:: stochastic_tree - :members: \ No newline at end of file +Methods +======= + +`lpy_treesim` marries biologically inspired L-systems with rule-based care +operations (tying, pruning, support placement) so you can synthesize +orchard-ready tree geometries. This page explains how the major pieces interact. + +Prototype-driven L-System +------------------------- + +`base_lpy.lpy` is the only grammar we execute at runtime. Rather than handcode +every rule, it reads extern variables that point to Python classes in +`examples/`: + +``prototype_dict_path`` + Imports the shared `basicwood_prototypes` mapping. Each entry contains a + `TreeBranch` subclass plus its `BasicWoodConfig`, which exposes stochastic + bending, bud spacing, lengths, and colors. + +``trunk_class_path`` + The entry class that seeds the axiom. All other branches sprout from the + prototypes configured on this trunk. + +``simulation_class_path`` / ``simulation_config_class_path`` + Provide training-system-specific behaviors (e.g., UFO trellis vs. Envy + spindle). The config dataclass is serialized and handed to the runtime + simulation via `TreeSimulationBase`. + +During each derivation step the grammar delegates to your Python classes to +decide when buds break, which prototype to spawn, and how to orient each new +segment. Because everything happens inside Python, you can use NumPy, random +sampling, and heuristics tailored to your target cultivar. + +Tying, pruning, and supports +--------------------------- + +The base simulation loop interleaves three routines: + +1. **Derive**: expand the L-system string using the prototype logic. +2. **Tie**: after a configurable number of iterations (`num_iteration_tie`), the + simulation attaches branches to the virtual support wires generated by + `TreeSimulationBase.generate_points()`. +3. **Prune**: after `num_iteration_prune`, branches that exceed thresholds (age, + curvature, or spacing) are removed. The decision functions live inside + `stochastic_tree.py` and can be augmented via your prototypes. + +All three phases reuse the `ColorManager` so instance labels remain stable even +after pruning removes geometry. + +Batch export pipeline +--------------------- + +`tree_generation/make_n_trees.py` wraps the grammar in a CLI tool: + +* Builds a fresh `Lsystem` object per tree and injects the extern dictionary. +* Seeds Python's RNG per tree so stochastic decisions differ while still being + reproducible when `--rng-seed` is provided. +* Calls `sceneInterpretation` to convert the final string into a PlantGL scene. +* Writes the mesh using `tree_generation.helpers.write`, which emits ASCII PLY + with normals and color attributes. +* Dumps `{namespace}_{tree_name}_{index:05d}_colors.json` containing the mapping + from instance IDs to semantic roles (spur, branch, trunk, tie, etc.). + +The naming scheme caps at 99,999 trees per run; start a new namespace if you +need more. + +Extending the system +-------------------- + +To add a brand-new method (new pruning heuristic, new tie behavior), derive from +the appropriate class in `simulation_base.py` or `stochastic_tree.py` and inject +your class via the extern dictionary. Because all extern paths are strings, +`make_n_trees.py` can consume any importable module without further code +changes. \ No newline at end of file diff --git a/docs/source/resources.rst b/docs/source/resources.rst index 701e438..d84bd9b 100644 --- a/docs/source/resources.rst +++ b/docs/source/resources.rst @@ -1,4 +1,29 @@ +Resources +========= -* TreeSim_Lpy Documentation - https://treesim-lpy.readthedocs.io/en/latest/ -* Lpy documentation - https://lpy.readthedocs.io/en/latest -* Lpy training material - https://github.com/fredboudon/lpy-training \ No newline at end of file +This section collects official links plus background material on L-systems and +training systems referenced by `lpy_treesim`. + +Project links +------------- + +- **GitHub**: `https://github.com/OSUrobotics/lpy_treesim `_ +- **Issue tracker**: Use the GitHub repo to report bugs or request new tree + templates. + +L-Py and Plant Modeling +----------------------- + +- **L-Py documentation**: `https://lpy.readthedocs.io/en/latest/ `_ +- **OpenAlea/PlantGL**: `https://openalea.readthedocs.io/en/latest/ `_ +- **L-Py training notebooks** (official tutorials): + `https://github.com/fredboudon/lpy-training `_ +- **Foundational paper**: Prusinkiewicz & Lindenmayer, *The Algorithmic Beauty + of Plants*, for a deep dive into L-systems. + +Supporting tooling +------------------ + +- **Conda documentation**: `https://docs.conda.io/en/latest/ `_ +- **Sphinx documentation**: `https://www.sphinx-doc.org/en/master/ `_ +- **PLY file format**: `http://paulbourke.net/dataformats/ply/ `_ (useful when post-processing meshes). \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst index ba3300b..185ab60 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -1,18 +1,239 @@ -====== Usage -====== +===== + +The typical workflow for `lpy_treesim` has three stages: + +1. Describe **prototypes** that capture the botanical building blocks for your + training system. +2. Provide a **simulation configuration** that tunes derivation length, pruning + passes, and support layout. +3. Feed both into the CLI generator (`tree_generation/make_n_trees.py`) to + batch-export `.ply` meshes and color maps. + +The following sections walk through each step with concrete file references so +you can add your own tree families next to the built-in `UFO` example. + +1. Define prototypes (deep dive) +-------------------------------- + +Prototype files live under ``examples//_prototypes.py``. They +describe the biological components of your tree by subclassing +``stochastic_tree.TreeBranch`` (ultimately ``BasicWood``). Study the existing UFO +implementation for a concrete template: ``examples/UFO/UFO_prototypes.py``. + +The critical building blocks are the four state dataclasses defined in +``stochastic_tree.py``: + +* ``LocationState`` tracks the start/end coordinates and the last tie point. +* ``TyingState`` stores tie axis, guide points, and the wire to attach to. +* ``GrowthState`` holds thickness increments, per-step growth length, and max + length. +* ``InfoState`` carries metadata such as age, order, prunability, and color. + +When you instantiate a prototype with ``BasicWoodConfig`` these states are +created for you. Your subclass is responsible for overriding the behavioral +hooks: + +* ``is_bud_break`` decides when a new bud/branch emerges. +* ``create_branch`` clones another prototype from ``basicwood_prototypes`` and + returns it. +* ``pre_bud_rule`` / ``post_bud_rule`` allow in-place adjustments to growth and + tying parameters. +* ``post_bud_rule`` can emit custom L-Py modules (e.g., ``@O`` for fruiting). + +Below is a simplified excerpt from the real UFO spur definition showing how the +pieces line up: + +.. code-block:: python + + from stochastic_tree import BasicWood, BasicWoodConfig + + basicwood_prototypes = {} + + class Spur(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict=None): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + return rd.random() < 0.1 * (1 - num_buds_segment / self.growth.max_buds_segment) + + def create_branch(self): + return None # spurs terminate growth + + def post_bud_rule(self, plant_segment, simulation_config): + radius = plant_segment.growth.thickness * simulation_config.thickness_multiplier + return [('@O', [float(radius)])] + + spur_config = BasicWoodConfig( + max_buds_segment=2, + growth_length=0.05, + cylinder_length=0.01, + thickness=0.003, + color=[0, 255, 0], + bud_spacing_age=1, + curve_x_range=(-0.2, 0.2), + curve_y_range=(-0.2, 0.2), + curve_z_range=(-1, 1), + ) + basicwood_prototypes['spur'] = Spur(config=spur_config, prototype_dict=basicwood_prototypes) + +Two more classes, ``Branch`` and ``Trunk``, reference the same dictionary when +spawning children: + +.. code-block:: python + + class Trunk(TreeBranch): + def create_branch(self): + if rd.random() > 0.1: + return Branch(copy_from=self.prototype_dict['branch']) + + branch_config = BasicWoodConfig( + tie_axis=(0, 0, 1), + thickness=0.01, + thickness_increment=1e-5, + growth_length=0.1, + color=[255, 150, 0], + bud_spacing_age=2, + ) + basicwood_prototypes['branch'] = Branch(config=branch_config, prototype_dict=basicwood_prototypes) + +Key implementation details to replicate: + +* Always pass ``prototype_dict=basicwood_prototypes`` when constructing each + prototype so clones reference the shared registry. +* Set ``BasicWoodConfig.tie_axis`` for the classes you expect to tie; the base + simulation will skip tying for branches whose tie axis is ``None``. +* Use ``BasicWoodConfig.color`` for per-instance labeling—the ``ColorManager`` + picks up these RGB triplets and writes them to the ``*_colors.json`` mapping. + +2. Configure simulation parameters +---------------------------------- + +The simulator pairs your prototypes with tie/prune logic by subclassing +`TreeSimulationBase` and `SimulationConfig` (see `simulation_base.py`). Each tree +family stores both classes in `examples//_simulation.py`. + +For example, ``examples/UFO/UFO_simulation.py`` implements both the config and +the runtime class: + +.. code-block:: python + + from simulation_base import SimulationConfig, TreeSimulationBase + + @dataclass + class UFOSimulationConfig(SimulationConfig): + num_iteration_tie: int = 8 + num_iteration_prune: int = 16 + pruning_age_threshold: int = 8 + derivation_length: int = 160 + support_trunk_wire_point: tuple = (0.6, 0, 0.4) + support_num_wires: int = 7 + ufo_x_range: tuple = (0.65, 3) + ufo_x_spacing: float = 0.3 + ufo_z_value: float = 1.4 + ufo_y_value: float = 0 + thickness_multiplier: float = 1.2 + semantic_label: bool = True + + class UFOSimulation(TreeSimulationBase): + def generate_points(self): + x = np.arange( + self.config.ufo_x_range[0], + self.config.ufo_x_range[1], + self.config.ufo_x_spacing, + ) + z = np.full((x.shape[0],), self.config.ufo_z_value) + y = np.full((x.shape[0],), self.config.ufo_y_value) + return list(zip(x, y, z)) + +``SimulationConfig`` enforces consistent behavior via ``__post_init__``—only one +labeling mode (semantic / instance / per-cylinder) can be true at a time. The +base class also exposes: + +* ``num_iteration_tie`` / ``num_iteration_prune``: cadence for maintenance. +* ``energy_distance_weight`` / ``energy_threshold``: scoring knobs for the + branch-to-wire assignment matrix built inside ``TreeSimulationBase.get_energy_mat``. +* ``pruning_age_threshold``: compared against ``branch.info.age`` in + ``TreeSimulationBase.prune`` before removing geometry via ``helper.cut_from``. + +On the runtime side, ``TreeSimulationBase`` supplies ready-to-use algorithms for +tying, pruning, and support assignment: + +* ``generate_points`` must return the actual wire coordinates used when tie + curves are computed (``BasicWood.update_guide``). +* ``tie`` walks the L-system string and calls ``branch.tie_lstring`` for one + eligible branch per invocation. +* ``prune`` removes untied branches whose age exceeds + ``config.pruning_age_threshold`` and whose prototype flag ``prunable`` is set. + +To bring up a new architecture, duplicate the UFO module, rename the classes to +``SimulationConfig`` / ``Simulation``, and add any extra +dataclass fields required for your geometry (wire spacing, tie axis overrides, +etc.). Ensure the class names match the paths you pass to ``make_n_trees.py``. + +Checklist for a new tree type: + +1. Copy `examples/UFO/UFO_simulation.py` to `examples//_simulation.py`. +2. Rename the dataclass to `SimulationConfig`. +3. Rename the runtime class to `Simulation` and override any helper + methods you need. +4. Ensure the module exposes the two symbols with those exact names so the CLI + resolver can import them. + +3. Batch-generate assets +------------------------ + +Once prototypes and simulations exist, the CLI script assembles everything. It +always loads `base_lpy.lpy` and expects your modules to live inside the +`examples` package. .. code-block:: bash - conda activate lpy - lpy + cd lpy_treesim + python lpy_treesim/tree_generation/make_n_trees.py \ + --tree_name UFO \ + --namespace orchardA \ + --num_trees 64 \ + --output_dir dataset/ufo_batch \ + --rng-seed 42 \ + --verbose + +Important flags: + +``--tree_name`` + The directory under `examples/` that contains both the prototype and + simulation modules (`examples/UFO`, `examples/Envy`, etc.). The script + automatically builds module paths such as + `examples.UFO.UFO_prototypes.basicwood_prototypes`. -Once you have launched the lpy GUI, navigate to whatever .lpy file you want to run and press Animate on the toolbar +``--namespace`` + Prefix for exported files. Meshes are named + ``{namespace}_{tree_name}_{index:05d}.ply`` and color maps are suffixed with + ``_colors.json``. Up to 99,999 indices are supported per run. -.. note:: - The tying/pruning processes will only work if you press Animate and not Run +``--rng-seed`` + Provides reproducible randomness while still using a different seed for each + tree inside the batch. + +Outputs include: + +- `.ply` meshes stored in the target directory. +- JSON color maps emitted by `ColorManager` so downstream segmentation models + can recover per-instance labeling. + +4. Inspect results (optional) +----------------------------- + +If you want to watch an individual tree evolve, run the same environment through +the L-Py GUI: + +.. code-block:: bash -.. warning:: - It is a known bug that sometimes, the files dont run properly when animated the first time. Press Rewind, and then press Animate to run it again. + conda activate lpy + lpy lpy_treesim/base_lpy.lpy - +Inside the GUI, set the extern variables (prototype paths, simulation classes, +`color_manager`, etc.) to match the CLI defaults or a custom configuration +dictionary. Use **Animate** rather than **Run** so tying/pruning hooks fire. diff --git a/examples/Envy/Envy.lpy b/examples/Envy/Envy.lpy new file mode 100644 index 0000000..8c5e2d8 --- /dev/null +++ b/examples/Envy/Envy.lpy @@ -0,0 +1,26 @@ +from pathlib import Path +from openalea.lpy import Lsystem +from openalea.lpy import * +from openalea.plantgl.all import * +from lpy_treesim import ColorManager + +BASE_LPY_PATH = Path(__file__).resolve().parents[2] / "base_lpy.lpy" + +color_manager = ColorManager() + +vars = { + "prototype_dict_path": "examples.Envy.Envy_prototypes.basicwood_prototypes", + "trunk_class_path": "examples.Envy.Envy_prototypes.Trunk", + "simulation_config_class_path": "examples.Envy.Envy_simulation.EnvySimulationConfig", + "simulation_class_path": "examples.Envy.Envy_simulation.EnvySimulation", + "color_manager": color_manager, + "axiom_pitch": 0.0, + "axiom_yaw": 0.0, +} + +lsystem = Lsystem(str(BASE_LPY_PATH), vars) + +for lstring in lsystem: + lsystem.plot(lstring) + +# color_manager.export_mapping("envy_color_mapping.json") \ No newline at end of file diff --git a/examples/Envy/Envy_prototypes.py b/examples/Envy/Envy_prototypes.py new file mode 100644 index 0000000..d5fb303 --- /dev/null +++ b/examples/Envy/Envy_prototypes.py @@ -0,0 +1,174 @@ +import sys +sys.path.append('../../') +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +import numpy as np +import random as rd +from dataclasses import dataclass +import copy +from openalea.lpy import newmodule +from helper import * + + +class Spur(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + return (rd.random() < 0.1 * (1 - num_buds_segment / self.growth.max_buds_segment)) + + def create_branch(self): + return None + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +class Branch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_break_buds): + if num_break_buds >= self.growth.max_buds_segment: + return False + return (rd.random() < 0.5 * (1 - num_break_buds / self.growth.max_buds_segment)) + + def create_branch(self): + if rd.random() > 0.8: + new_ob = NonTrunk(copy_from=self.prototype_dict['nontrunk']) + else: + new_ob = Spur(copy_from=self.prototype_dict['spur']) + return new_ob + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +class Trunk(TreeBranch): + """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + if (rd.random() > 0.1 * (1 - num_buds_segment / self.growth.max_buds_segment)): + return False + return True + + def create_branch(self): + if rd.random() > 0.8: + return Spur(copy_from=self.prototype_dict['spur']) + else: + return Branch(copy_from=self.prototype_dict['branch']) + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +class NonTrunk(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + return (rd.random() < 0.5 * (1 - num_buds_segment / self.growth.max_buds_segment)) + + def create_branch(self): + if rd.random() > 0.3: + return None + else: + new_ob = Spur(copy_from=self.prototype_dict['spur']) + return new_ob + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +# growth_length = 0.1 +basicwood_prototypes = {} + +# Create configs for cleaner prototype setup +spur_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=None, + max_length=0.2, + thickness=0.003, + growth_length=0.05, + cylinder_length=0.05, + thickness_increment=0., + color=[0, 255, 0], + bud_spacing_age=2, + curve_x_range=(-0.2, 0.2), + curve_y_range=(-0.2, 0.2), + curve_z_range=(-1, 1), + prunable=True +) + +branch_config = BasicWoodConfig( + max_buds_segment=2, + tie_axis=(1, 0, 0), + max_length=2.2, + thickness=0.01, + growth_length=0.1, + cylinder_length=0.05, + thickness_increment=0.00001, + color=[255, 150, 0], + bud_spacing_age=2, + curve_x_range=(-0.5, 0.5), + curve_y_range=(-0.5, 0.5), + curve_z_range=(-1, 1), + prunable=True +) + +trunk_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=None, + max_length=4, + thickness=0.01, + growth_length=0.1, + cylinder_length=0.05, + thickness_increment=0.00001, + color=[255, 0, 0], + bud_spacing_age=2, + curve_x_range=(-1, 1), + curve_y_range=(-0.15, 0.15), + curve_z_range=(0, 10), + prunable=False +) + +nontrunk_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=None, + max_length=0.3, + thickness=0.003, + growth_length=0.05, + cylinder_length=0.05, + thickness_increment=0.00001, + color=[0, 255, 0], + bud_spacing_age=2, + curve_x_range=(-0.5, 0.5), + curve_y_range=(-0.5, 0.5), + curve_z_range=(-1, 1), + prunable=True +) + +# Setup prototypes using configs +basicwood_prototypes['spur'] = Spur(config=spur_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['branch'] = Branch(config=branch_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['trunk'] = Trunk(config=trunk_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['nontrunk'] = NonTrunk(config=nontrunk_config, prototype_dict=basicwood_prototypes) \ No newline at end of file diff --git a/examples/Envy/Envy_simulation.py b/examples/Envy/Envy_simulation.py new file mode 100644 index 0000000..bb8a0d3 --- /dev/null +++ b/examples/Envy/Envy_simulation.py @@ -0,0 +1,64 @@ +import sys +sys.path.append('../../') +from dataclasses import dataclass +import numpy as np +from simulation_base import SimulationConfig, TreeSimulationBase + +@dataclass +class EnvySimulationConfig(SimulationConfig): + """Configuration for Envy trellis tree simulation parameters.""" + + # Override base defaults for Envy-specific values + num_iteration_tie: int = 5 + num_iteration_prune: int = 16 + pruning_age_threshold: int = 6 + derivation_length: int = 128 + + # Envy-specific Support Structure + support_trunk_wire_point = None + support_num_wires: int = 14 + + # Envy-specific Point Generation (V-trellis) + trellis_x_value: float = 0.45 + trellis_z_start: float = 0.6 + trellis_z_end: float = 3.4 + trellis_z_spacing: float = 0.45 + + # Envy-specific Growth Parameters + semantic_label: bool = True + instance_label: bool = False + per_cylinder_label: bool = False + + +class EnvySimulation(TreeSimulationBase): + """ + Envy trellis architecture simulation. + + Implements the Envy V-trellis training system with wires arranged in a V-shape + on both sides of the tree row. + """ + + def generate_points(self): + """ + Generate 3D points for the V-trellis wire structure. + + Creates a linear array of wire attachment points along the x-axis at a fixed + height (z) and depth (y). The points are spaced evenly within the configured + z-range and used to construct the trellis support structure. + + Returns: + list: List of (x, y, z) tuples representing wire attachment points in V-trellis formation + """ + x = np.full((7,), self.config.trellis_x_value).astype(float) + y = np.full((7,), 0).astype(float) + z = np.arange(self.config.trellis_z_start, + self.config.trellis_z_end, + self.config.trellis_z_spacing) + + pts = [] + for i in range(x.shape[0]): + pts.append((-x[i], y[i], z[i])) + pts.append((x[i], y[i], z[i])) + return pts + + diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy new file mode 100644 index 0000000..dfc4fe9 --- /dev/null +++ b/examples/UFO/UFO.lpy @@ -0,0 +1,26 @@ +from pathlib import Path +from openalea.lpy import Lsystem +from openalea.lpy import * +from openalea.plantgl.all import * +from lpy_treesim import ColorManager + +BASE_LPY_PATH = Path(__file__).resolve().parents[2] / "base_lpy.lpy" + +color_manager = ColorManager() + +vars = { + "prototype_dict_path": "examples.UFO.UFO_prototypes.basicwood_prototypes", + "trunk_class_path": "examples.UFO.UFO_prototypes.Trunk", + "simulation_config_class_path": "examples.UFO.UFO_simulation.UFOSimulationConfig", + "simulation_class_path": "examples.UFO.UFO_simulation.UFOSimulation", + "color_manager": color_manager, + "axiom_pitch": 270.0, + "axiom_yaw": 0.0, +} + +lsystem = Lsystem(str(BASE_LPY_PATH), vars) + +for lstring in lsystem: + lsystem.plot(lstring) + +# color_manager.export_mapping("ufo_color_mapping.json") diff --git a/examples/UFO/UFO_prototypes.py b/examples/UFO/UFO_prototypes.py new file mode 100644 index 0000000..82c1077 --- /dev/null +++ b/examples/UFO/UFO_prototypes.py @@ -0,0 +1,182 @@ +import sys +sys.path.append('../../') +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +import numpy as np +import random as rd +from dataclasses import dataclass +import copy +from openalea.lpy import newmodule +from helper import * +from openalea.lpy import Lsystem, AxialTree, newmodule +class Spur(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + return (rd.random() < 0.1 * (1 - num_buds_segment / self.growth.max_buds_segment)) + + def create_branch(self): + return None + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + radius = plant_segment.growth.thickness * simulation_config.thickness_multiplier + # return L-Py module directly + # from openalea.lpy import newModule + return [('@O', [float(radius)])] + + + +class TertiaryBranch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + if (rd.random() < 0.005*self.growth.growth_length * (1 - num_buds_segment / self.growth.max_buds_segment)): + return True + + def create_branch(self): + if rd.random()>0.8: + new_ob = Branch(copy_from = self.prototype_dict['side_branch']) + else: + new_ob = Spur(copy_from = self.prototype_dict['spur']) + return new_ob + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + +class Branch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + if (rd.random() < 0.2 * (1 - num_buds_segment / self.growth.max_buds_segment)): + + return True + + def create_branch(self): + try: + if rd.random()>0.9: + new_ob = TertiaryBranch(copy_from = self.prototype_dict['side_branch']) + else: + new_ob = Spur(copy_from = self.prototype_dict['spur']) + except: + return None + return new_ob + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +class Trunk(TreeBranch): + """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + if (rd.random() > 0.05*self.length/self.growth.max_length * (1 - num_buds_segment / self.growth.max_buds_segment)): + return False + return True + + def create_branch(self): + if rd.random() > 0.1: + return Branch(copy_from = self.prototype_dict['branch']) + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config ): + return None + + + +# growth_length = 0.1 +basicwood_prototypes = {} + +# Create configs for cleaner prototype setup +spur_config = BasicWoodConfig( + max_buds_segment=2, + tie_axis=None, + max_length=0.1, + thickness=0.003, + growth_length=0.05, + cylinder_length=0.01, + thickness_increment=0., + color=[0, 255, 0], + bud_spacing_age=1, # Spurs bud every 1 age unit + curve_x_range=(-0.2, 0.2), # Tighter bounds for spur curves + curve_y_range=(-0.2, 0.2), # Tighter bounds for spur curves + curve_z_range=(-1, 1) # Same Z range +) + +side_branch_config = BasicWoodConfig( + max_buds_segment=2, + tie_axis=None, + max_length=0.25, + thickness=0.003, + growth_length=0.05, + cylinder_length=0.01, + thickness_increment=0.00001, + color=[0, 255, 0], + bud_spacing_age=2, # Tertiary branches bud every 3 age units + curve_x_range=(-0.5, 0.5), # Moderate bounds for tertiary branches + curve_y_range=(-0.5, 0.5), # Moderate bounds for tertiary branches + curve_z_range=(-1, 1) # Same Z range +) + +trunk_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=(1, 0, 0), + max_length=3, + thickness=0.02, + thickness_increment=0.00001, + growth_length=0.1, + cylinder_length=0.02, + color=[255, 0, 0], + bud_spacing_age=2, # Trunk buds every 4 age units + curve_x_range=(-0.3, 0.3), # Conservative bounds for trunk + curve_y_range=(-0.3, 0.3), # Conservative bounds for trunk + curve_z_range=(-0.5, 0.5), # Tighter Z range for trunk + prunable=False +) + +branch_config = BasicWoodConfig( + max_buds_segment=2, + tie_axis=(0, 0, 1), + max_length=2.5, + thickness=0.01, + thickness_increment=0.00001, + growth_length=0.1, + cylinder_length=0.02, + color=[255, 150, 0], + bud_spacing_age=2, # Branches bud every 2 age units + curve_x_range=(-0.4, 0.4), # Moderate bounds for primary branches + curve_y_range=(-0.4, 0.4), # Moderate bounds for primary branches + curve_z_range=(-1, 1) # Same Z range +) + +# Setup prototypes using configs +basicwood_prototypes['spur'] = Spur(config=spur_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['side_branch'] = TertiaryBranch(config=side_branch_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['trunk'] = Trunk(config=trunk_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['branch'] = Branch(config=branch_config, prototype_dict=basicwood_prototypes) + + + + \ No newline at end of file diff --git a/examples/UFO/UFO_simulation.py b/examples/UFO/UFO_simulation.py new file mode 100644 index 0000000..0578159 --- /dev/null +++ b/examples/UFO/UFO_simulation.py @@ -0,0 +1,66 @@ +import sys +sys.path.append('../../') +from dataclasses import dataclass +import numpy as np +from simulation_base import SimulationConfig, TreeSimulationBase + +@dataclass +class UFOSimulationConfig(SimulationConfig): + """Configuration for UFO trellis tree simulation parameters.""" + + # Override base defaults for UFO-specific values + num_iteration_tie: int = 8 + num_iteration_prune: int = 16 + pruning_age_threshold: int = 8 + derivation_length: int = 160 + + # UFO-specific Support Structure + support_trunk_wire_point: tuple = (0.6, 0, 0.4) + support_num_wires: int = 7 + + # UFO-specific Point Generation + ufo_x_range: tuple = (0.65, 3) + ufo_x_spacing: float = 0.3 + ufo_z_value: float = 1.4 + ufo_y_value: float = 0 + + # UFO-specific Growth Parameters + thickness_multiplier: float = 1.2 # Multiplier for internode thickness + semantic_label: bool = True + instance_label: bool = False + per_cylinder_label: bool = False + + +class UFOSimulation(TreeSimulationBase): + """ + UFO trellis architecture simulation. + + Implements the UFO (Upright Fruiting Offshoots) training system with + horizontal wires arranged linearly along the x-axis. + """ + + def generate_points(self): + """ + Generate 3D points for the UFO trellis wire structure. + + Creates a linear array of wire attachment points along the x-axis at a fixed + height (z) and depth (y). The points are spaced evenly within the configured + x-range and used to construct the trellis support structure. + + Returns: + list: List of (x, y, z) tuples representing wire attachment points, + where all points share the same y and z coordinates. + """ + x = np.arange( + self.config.ufo_x_range[0], + self.config.ufo_x_range[1], + self.config.ufo_x_spacing + ).astype(float) + z = np.full((x.shape[0],), self.config.ufo_z_value).astype(float) + y = np.full((x.shape[0],), self.config.ufo_y_value).astype(float) + + wire_attachment_points = [] + for point_index in range(x.shape[0]): + wire_attachment_points.append((x[point_index], y[point_index], z[point_index])) + + return wire_attachment_points diff --git a/examples/UFO_tie_prune_label.lpy b/examples/UFO_tie_prune_label.lpy deleted file mode 100644 index 5a17163..0000000 --- a/examples/UFO_tie_prune_label.lpy +++ /dev/null @@ -1,349 +0,0 @@ -""" -Tying, Pruning and lablelling UFO architecture trees -""" -import sys -sys.path.append('../') -from stochastic_tree import Support, BasicWood -import numpy as np -import random as rd -import copy -import gc -import times -from helper import * - -# Used to set the contour back to a circle after it has been changed for a branch -def reset_contour(): - default_curve = create_noisy_circle_curve(1, 0, 30) # Example of a simple default contour - nproduce SetContour(default_curve) - -class Spur(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Spur.count+=1 - self.contour = create_noisy_circle_curve(1, .2, 30) - - def is_bud_break(self, num_buds_segment): - if num_buds_segment >= self.max_buds_segment: - return False - return (rd.random() < 0.1) - - def create_branch(self): - return None - - def grow(self): - pass - - -class LittleBranch(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 40, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - LittleBranch.count+=1 - self.num_buds = 0 - self.contour = create_noisy_circle_curve(1, .2, 30) - - def is_bud_break(self, num_buds_segment): - if num_buds_segment >= 2: - return False - if (rd.random() < 0.005*self.growth_length*(1 - self.num_buds/self.max_buds_segment)): - self.num_buds +=1 - return True - - def create_branch(self): - if rd.random()>0.8: - new_ob = Branch(copy_from = self.prototype_dict['side_branch']) - else: - new_ob = Spur(copy_from = self.prototype_dict['spur']) - return new_ob - - def grow(self): - pass - -class Branch(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 140, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Branch.count+=1 - self.num_buds = 0 - self.contour = create_noisy_circle_curve(1, .2, 30) - - def is_bud_break(self, num_buds_segment): - if num_buds_segment >= 2: - return False - if (rd.random() < 0.2*(1 - self.num_buds/self.max_buds_segment)): - - return True - - def create_branch(self): - try: - if rd.random()>0.9: - new_ob = LittleBranch(copy_from = self.prototype_dict['side_branch']) - else: - new_ob = Spur(copy_from = self.prototype_dict['spur']) - except: - return None - return new_ob - - def grow(self): - pass - - -class Trunk(BasicWood): - count = 0 - """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ - def __init__(self, copy_from = None, max_buds_segment: int = 60, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 0, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Trunk.count+=1 - self.num_buds = 0 - self.contour = create_noisy_circle_curve(1, .2, 30) - - def is_bud_break(self, num_buds_segment): - if num_buds_segment >= self.max_buds_segment: - return False - if (rd.random() > 0.05*self.length/self.max_length*(1 - self.num_buds/self.max_buds_segment)): - return False - self.num_buds+=1 - return True - - def create_branch(self): - if rd.random() > 0.1: - return Branch(copy_from = self.prototype_dict['branch']) - - def grow(self): - pass - - - - -#Pass transition probabs? --> solve with abstract classes -growth_length = 0.1 -basicwood_prototypes = {} -basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.1, thickness = 0.003, growth_length = growth_length/2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0, 255, 0]) -basicwood_prototypes['side_branch'] = LittleBranch(tie_axis = (1,1,0), max_length = 0.25, thickness = 0.003, growth_length =growth_length/2, thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [0, 255, 0]) - -basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 3, thickness = 0.02, thickness_increment = 0.00001, growth_length = growth_length, prototype_dict = basicwood_prototypes, color = [255, 0, 0]) -basicwood_prototypes['branch'] = Branch(tie_axis = (1,1,0), max_length = 2.5, thickness = 0.01, thickness_increment = 0.00001, growth_length = growth_length, prototype_dict = basicwood_prototypes, color = [255, 150, 0]) - -#init -trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) -time_count = 0 - -def generate_points_ufo(): - x = np.arange(0.65, 3, 0.3).astype(float) - z = np.full((x.shape[0],), 1.4).astype(float) - y = np.full((x.shape[0],), 0).astype(float) - pts = [] - id = 0 - for i in range(x.shape[0]): - pts.append((x[i], y[i], z[i])) - id+=1 - - return pts - - - -support = Support(generate_points_ufo(), 7 , 1 , (0.6,0,0.4), (0,1,1), (0,1,1)) -num_iteration_tie = 8 -num_iteration_prune = 16 -bud_spacing_age = 2 -trunk_base.guide_target = support.trunk_wire -###Tying stuff begins - -def ed(a,b): - return (a[0]-b[0])**2+(a[1]-b[1])**2+(a[2]-b[2])**2 - -def get_energy_mat(branches, arch): - num_branches = len(branches) - num_wires = len(list(arch.branch_supports.values())) - energy_matrix = np.ones((num_branches,num_wires))*np.inf - for branch_id, branch in enumerate(branches): - if branch.has_tied: - continue - for wire_id, wire in arch.branch_supports.items(): - if wire.num_branch>=1: - continue - energy_matrix[branch_id][wire_id] = ed(wire.point, branch.start)/2+ed(wire.point,branch.end)/2#,+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age) - return energy_matrix - -def decide_guide(energy_matrix, branches, arch): - for i in range(energy_matrix.shape[0]): - min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) - if energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 1 : - return - if not (branches[min_arg[0][0]].has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): - branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) - arch.branch_supports[min_arg[0][1]].add_branch() # Increment the number of branches tied to the wire - - #trellis_wires.trellis_pts[min_arg[0][1]].num_branches+=1 - for j in range(energy_matrix.shape[1]): - energy_matrix[min_arg[0][0]][j] = np.inf - for j in range(energy_matrix.shape[0]): - energy_matrix[j][min_arg[0][1]] = np.inf - -def tie(lstring): - for j,i in enumerate(lstring): - if (i == 'C' and i[0].type.__class__.__name__ == 'Branch') or i == 'T' : - if i[0].type.tie_updated == False: - continue - curr = i[0] - if i[0].type.guide_points: - i[0].type.tie_updated = False - i[0].type.guide_target.add_branch() - lstring, count = i[0].type.tie_lstring(lstring, j) - - return True - return False - - -def StartEach(lstring): - global parent_child_dict, support, trunk_base - if support.trunk_wire and trunk_base.tie_updated == False: - trunk_base.tie_update() - - for i in parent_child_dict[trunk_base.name]: - if i.tie_updated == False: - i.tie_update() - - -def EndEach(lstring): - global parent_child_dict, support, num_iteration_tie - - tied = False - if (getIterationNb()+1)%num_iteration_tie == 0: - - if support.trunk_wire : - trunk_base.update_guide(trunk_base.guide_target) #Tie trunk one iteration before branches - energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) - decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) - for branch in parent_child_dict[trunk_base.name]: - branch.update_guide(branch.guide_target) - while tie(lstring): - pass - if (getIterationNb() + 1) % num_iteration_prune == 0: - while pruning_strategy(lstring): # Prune branches until no more can be pruned - pass - return lstring - -def pruning_strategy(lstring): #Remove remnants of cut - cut = False - for j,i in enumerate(lstring): - if i.name == 'C' and i[0].type.age > 8 and i[0].type.has_tied == False and i[0].type.cut == False: - i[0].type.cut = True - lstring = cut_from(j, lstring) - return True - return False - -parent_child_dict = {} -parent_child_dict[trunk_base.name] = [] -label = True -#Tie trunk -module Attractors -module grow_object -module bud -module branch -module C -curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) -Axiom: Attractors(support)SetGuide(curve, trunk_base.max_length)[GetPos(trunk_base.start)T(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.end)] -derivation length: 160 - -production: -#Decide whether branch internode vs trunk internode need to be the same size. -grow_object(o) : - if o == None: - produce * - if o.length >= o.max_length: - nproduce * - else: - nproduce SetContour(o.contour) - o.grow_one() - if label: - r, g, b = o.color - nproduce SetColor(r,g,b) - if 'Spur' in o.name: - produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))@O(o.thickness*1.2)grow_object(o) - else: - nproduce I(o.growth_length, o.thickness, o) - if np.isclose(o.age%bud_spacing_age,0, atol = 0.01): - nproduce bud(ParameterSet(type = o, num_buds = 0)) - produce grow_object(o) - #produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))grow_object(o) - -bud(t) : - if t.type.is_bud_break(t.num_buds): - new_object = t.type.create_branch() - if new_object == None: - produce * - parent_child_dict[new_object.name] = [] - parent_child_dict[t.type.name].append(new_object) - #Store new object somewhere - t.num_buds+=1 - t.type.num_branches+=1 - - if 'LittleBranch' in new_object.name: - import time - curve = create_bezier_curve(x_range=(-.5, .5), y_range=(-.5, .5), z_range=(-1, 1), seed_val=time.time()) - nproduce[SetGuide(curve, new_object.max_length) - elif 'Spur' in new_object.name: - import time - curve = create_bezier_curve(x_range=(-.2, .2), y_range=(-.2, .2), z_range=(-1, 1), seed_val=time.time()) - nproduce[SetGuide(curve, new_object.max_length) - else: - nproduce [ - nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.end)]bud(t) - - -I(s,r,o) --> I(s,r+o.thickness_increment, o) -_(r) --> _(r+o.thickness_increment) - -homomorphism: - -I(a,r,o) --> F(a,r) -S(a,r,o) --> F(a,r) - -production: -Attractors(support): - pttodisplay = support.attractor_grid.get_enabled_points() - if len(pttodisplay) > 0: - produce [,(3) @g(PointSet(pttodisplay,width=10))] diff --git a/helper.py b/helper.py index 454cfb3..5ffefc2 100644 --- a/helper.py +++ b/helper.py @@ -1,126 +1,303 @@ +""" +Helper utilities for L-Py tree simulation system. + +This module provides utility functions for procedural tree generation and simulation +in the L-Py framework. It includes functions for: + +- L-System string manipulation (cutting, pruning operations) +- Geometric shape generation (contours, curves, noise patterns) +- Tree training and optimization utilities +- PlantGL integration for 3D visualization + +The functions in this module are used by various tree architecture implementations +(UFO, Envy, etc.) to perform common operations like branch pruning, wire attachment +optimization, and geometric shape generation for realistic tree modeling. +""" + from openalea.plantgl.all import NurbsCurve, Vector3, Vector4, Point4Array, Point2Array, Point3Array, Polyline2D, BezierCurve, BezierCurve2D from openalea.lpy import Lsystem, newmodule from random import uniform, seed from numpy import linspace, pi, sin, cos +import numpy as np +from typing import Callable, Dict, Iterable +import importlib + +def cut_from(pruning_position, lstring, lsystem_path=None): + """ + Mark a position in the L-System string for cutting/pruning. -def amplitude(x): return 2 - -def cut_from(pruning_id, s, path = None): - """Check cut_string_from_manipulation for manual implementation""" - # s.insertAt(pruning_id, newmodule('F')) - s.insertAt(pruning_id+1, newmodule('%')) - return s - -def cut_using_string_manipulation(pruning_id, s, path = None): - """Cuts starting from index pruning_id until branch - end signified by ']' or the entire subtrees if pruning_id starts from leader""" - bracket_balance = 0 - cut_num = pruning_id - #s[cut_num].append("no cut") - cut_num += 1 - pruning_id +=1 - total_length = len(s) - while(pruning_id < total_length): - if s[cut_num].name == '[': - bracket_balance+=1 - if s[cut_num].name == ']': - if bracket_balance == 0: - break - else: - bracket_balance-=1 - del s[cut_num] - pruning_id+=1 # Insert new node cut at the end of cut - if path != None: - new_lsystem = Lsystem(path) #Figure out to include time in this - new_lsystem.axiom = s - return new_lsystem - #s.insertAt(cut_num, newmodule("I(1, 0.05)")) - return s - -def pruning_strategy(it, lstring): - if((it+1)%8 != 0): + Inserts a cut marker (%) after the specified pruning position in the + L-System string. This marks the location where a branch should be + removed during the pruning process. + + Args: + pruning_position: Index in the L-System string where pruning should occur + lstring: The L-System string to modify + lsystem_path: Optional path to create a new L-System object (unused in current implementation) + + Returns: + Modified L-System string with cut marker inserted + """ + # Insert cut marker (%) after the pruning position + lstring.insertAt(pruning_position + 1, newmodule('%')) return lstring - cut = False - curr = 0 - while curr < len(lstring): - if lstring[curr] == '/': - if not (angle_between(lstring[curr].args[0], 0, 50) or angle_between(lstring[curr].args[0], 130, 180)): - if(len(lstring[curr].args) > 1): - if lstring[curr].args[1] == "no cut": - curr+=1 - continue - - # print("Cutting", curr, lstring[curr], (lstring[curr].args[0]+180)) - #lstring[curr].append("no cut") - lstring = cut_from(curr+1, lstring) - elif lstring[curr] == '&': - if not (angle_between(lstring[curr].args[0], 0, 50) or angle_between(lstring[curr].args[0], 130, 180)): - if(len(lstring[curr].args) > 1): - if lstring[curr].args[1] == "no cut": - curr+=1 - continue - # print("Cutting", curr, lstring[curr], (lstring[curr].args[0]+180)) - #lstring[curr].append("no cut") - lstring = cut_from(curr+1, lstring) - curr+=1 - - return lstring -def angle_between(angle, min, max): - angle = (angle+90) - if angle > min and angle < max: - return True - return False - -def myrandom(radius): - return uniform(-radius,radius) - -def gen_noise_branch(radius,nbp=20): - return NurbsCurve([(0,0,0,1),(0,0,1/float(nbp-1),1)]+[(myrandom(radius*amplitude(pt/float(nbp-1))), - myrandom(radius*amplitude(pt/float(nbp-1))), - pt/float(nbp-1),1) for pt in range(2,nbp)], - degree=min(nbp-1,3),stride=nbp*100) - -def create_noisy_circle_curve(radius, noise_factor, num_points=100, seed=None): - if seed is not None: - seed(seed) - t = linspace(0, 2 * pi, num_points, endpoint=False) - points = [] - for angle in t: - # Base circle points - x = radius * cos(angle) - y = radius * sin(angle) - - # Add noise - noise_x = uniform(-noise_factor, noise_factor) - noise_y = uniform(-noise_factor, noise_factor) - - noisy_x = x + noise_x - noisy_y = y + noise_y - - points.append((noisy_x, noisy_y)) - - # Ensure the curve is closed by adding the first point at the end - points.append(points[0]) +def cut_using_string_manipulation(pruning_position, lstring, lsystem_path=None): + """ + Remove a complete branch segment from the L-System string. + + Cuts starting from the pruning position until the end of the branch segment, + which is signified by a closing bracket ']'. Uses bracket balancing to handle + nested branch structures correctly. + + Args: + pruning_position: Starting index in the L-System string for the cut operation + lstring: The L-System string to modify + lsystem_path: Optional path to create a new L-System object with the modified string + + Returns: + Modified L-System string with the branch segment removed, or a new L-System + object if lsystem_path is provided + """ + bracket_balance = 0 + current_position = pruning_position + # Skip the pruning position itself + current_position += 1 + search_position = pruning_position + 1 + total_length = len(lstring) + + # Traverse the string until we find the matching closing bracket + while search_position < total_length: + if lstring[current_position].name == '[': + bracket_balance += 1 + elif lstring[current_position].name == ']': + if bracket_balance == 0: + # Found the matching closing bracket, stop here + break + else: + bracket_balance -= 1 + + # Remove the current element + del lstring[current_position] + search_position += 1 + + # If a path is provided, create a new L-System object + if lsystem_path is not None: + new_lsystem = Lsystem(lsystem_path) + new_lsystem.axiom = lstring + return new_lsystem + + return lstring + + +def angle_between(angle, min_angle, max_angle): + """ + Check if an angle falls within a specified range after 90-degree offset. + + Applies a 90-degree offset to the input angle and checks if the result + falls within the specified range. This is used for determining acceptable + tropism angles in the pruning strategy. + + Args: + angle: Input angle in degrees + min_angle: Minimum angle of the acceptable range (after offset) + max_angle: Maximum angle of the acceptable range (after offset) + + Returns: + bool: True if the offset angle is within the range, False otherwise + """ + offset_angle = angle + 90 + return min_angle <= offset_angle <= max_angle - # Create the PlantGL Point2Array and Polyline2D - curve_points = Point2Array(points) - curve = Polyline2D(curve_points) - return curve - -def create_bezier_curve(num_control_points=6, x_range=(-2,2), y_range=(-2, 2), z_range = (0, 10), seed_val=None): - if seed_val is not None: - seed(seed_val) # Set the random seed for reproducibility - # Generate progressive control points within the specified ranges +def generate_random_offset(radius): + """ + Generate a random offset value within a specified radius range. + + Creates a random float value between -radius and +radius, useful for + adding noise or variation to geometric shapes and curves. + + Args: + radius: Maximum absolute value for the random offset + + Returns: + float: Random value between -radius and +radius + """ + return uniform(-radius, radius) + +def generate_noisy_branch_curve(radius, num_control_points=20): + """ + Generate a NURBS curve representing a noisy branch shape. + + Creates a 3D NURBS curve with noise applied to create a natural-looking + branch shape. The curve starts at the origin and extends along the z-axis, + with x and y coordinates perturbed by noise that scales with distance. + + Args: + radius: Base radius for noise generation + num_control_points: Number of control points for the NURBS curve + + Returns: + NurbsCurve: PlantGL NURBS curve object representing the noisy branch + """ + # Create control points with progressive noise + control_points = [(0, 0, 0, 1), (0, 0, 1/float(num_control_points-1), 1)] + + for point_index in range(2, num_control_points): + t = point_index / float(num_control_points - 1) + noise_scale = radius * 2 # amplitude scaling factor + + x_noise = generate_random_offset(noise_scale) + y_noise = generate_random_offset(noise_scale) + + control_points.append((x_noise, y_noise, t, 1)) + + return NurbsCurve(control_points, degree=min(num_control_points-1, 3), stride=num_control_points*100) + +def create_noisy_branch_contour(radius, noise_factor, num_points=100, seed_value=None): + """ + Create a noisy 2D contour for branch cross-sections. + + Generates a circular contour with added noise to create natural-looking + branch cross-section shapes. The contour is closed and can be used for + extruding 3D branch geometry. + + Args: + radius: Base radius of the circular contour + noise_factor: Scale factor for the noise added to the contour + num_points: Number of points in the contour (higher = smoother) + seed_value: Random seed for reproducible results + + Returns: + Polyline2D: PlantGL 2D polyline representing the noisy contour + """ + if seed_value is not None: + seed(seed_value) + + # Generate angles around the circle + angles = linspace(0, 2 * pi, num_points, endpoint=False) + contour_points = [] + + for angle in angles: + # Calculate base circle coordinates + x_base = radius * cos(angle) + y_base = radius * sin(angle) + + # Add noise to create irregular shape + x_noise = uniform(-noise_factor, noise_factor) + y_noise = uniform(-noise_factor, noise_factor) + + x_noisy = x_base + x_noise + y_noisy = y_base + y_noise + + contour_points.append((x_noisy, y_noisy)) + + # Close the contour by repeating the first point + contour_points.append(contour_points[0]) + + # Create PlantGL geometry + point_array = Point2Array(contour_points) + return Polyline2D(point_array) + +def create_bezier_curve(num_control_points=6, x_range=(-2, 2), y_range=(-2, 2), z_range=(0, 10), seed_value=None): + """ + Create a randomized 3D Bezier curve for growth guidance. + + Generates a Bezier curve with randomly positioned control points within + specified ranges. The curve progresses along the z-axis with control points + distributed evenly in the z-direction but randomly in x and y. + + Args: + num_control_points: Number of control points for the Bezier curve + x_range: Tuple (min_x, max_x) defining the x-coordinate range + y_range: Tuple (min_y, max_y) defining the y-coordinate range + z_range: Tuple (min_z, max_z) defining the z-coordinate range + seed_value: Random seed for reproducible curve generation + + Returns: + BezierCurve: PlantGL Bezier curve object for growth guidance + """ + if seed_value is not None: + seed(seed_value) + + # Generate control points with progressive z-coordinates + z_values = linspace(z_range[0], z_range[1], num_control_points) control_points = [] - zs = linspace(z_range[0], z_range[1], num_control_points) - for i in range(num_control_points): - x = uniform(*x_range) - y = uniform(*y_range) - control_points.append(Vector4(x, y, zs[i], 1)) # Set z to 0 for 2D curve - # Create a Point3Array from the control points - control_points_array = Point4Array(control_points) - # Create and return the BezierCurve2D object - bezier_curve = BezierCurve(control_points_array) - return bezier_curve \ No newline at end of file + + for z_value in z_values: + x_coord = uniform(x_range[0], x_range[1]) + y_coord = uniform(y_range[0], y_range[1]) + control_points.append(Vector4(x_coord, y_coord, z_value, 1)) + + # Create PlantGL Bezier curve + control_point_array = Point4Array(control_points) + return BezierCurve(control_point_array) + + +def should_bud(plant_segment, simulation_config): + """Determine if a plant segment should produce a bud""" + return np.isclose(plant_segment.info.age % plant_segment.bud_spacing_age, 0, + atol=simulation_config.tolerance) + + +def start_each_common( + lstring, + branch_hierarchy: Dict[str, Iterable], + trellis_support, + main_trunk, +): + """Shared pre-iteration tying preparation logic.""" + del lstring # unused in shared logic; kept for L-Py parity + + if trellis_support.trunk_wire and not main_trunk.tying.tie_updated: + main_trunk.tie_update() + + for branch in branch_hierarchy[main_trunk.name]: + if not branch.tying.tie_updated: + branch.tie_update() + + +def end_each_common( + lstring, + branch_hierarchy: Dict[str, Iterable], + trellis_support, + tying_interval_iterations: int, + pruning_interval_iterations: int, + simulation_config, + main_trunk, + get_iteration_number: Callable[[], int], + get_energy_matrix, + decide_guide_fn, + tie_fn, + prune_fn, +): + """Shared post-iteration tying and pruning orchestration.""" + current_iteration = get_iteration_number() + 1 + + if current_iteration % tying_interval_iterations == 0: + if trellis_support.trunk_wire: + main_trunk.update_guide(main_trunk.tying.guide_target) + + branches = branch_hierarchy[main_trunk.name] + energy_matrix = get_energy_matrix(branches, trellis_support, simulation_config) + + decide_guide_fn(energy_matrix, branches, trellis_support, simulation_config) + + for branch in branches: + branch.update_guide(branch.tying.guide_target) + + while tie_fn(lstring, simulation_config): + pass + + if current_iteration % pruning_interval_iterations == 0: + while prune_fn(lstring, simulation_config): + pass + + return lstring + + +def resolve_attr(path: str): + """Import a fully qualified attribute path.""" + pkg_path, attr_name = path.rsplit('.', 1) + pkg = importlib.import_module(pkg_path) + return getattr(pkg, attr_name) diff --git a/examples/Camp_Envy_tie_prune_label.lpy b/other_files/legacy/Camp_Envy_tie_prune_label.lpy similarity index 87% rename from examples/Camp_Envy_tie_prune_label.lpy rename to other_files/legacy/Camp_Envy_tie_prune_label.lpy index 1804e6a..2f3dc6f 100644 --- a/examples/Camp_Envy_tie_prune_label.lpy +++ b/other_files/legacy/Camp_Envy_tie_prune_label.lpy @@ -83,7 +83,7 @@ class Spur(BasicWood): self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Spur.count+=1 self.num_leaves = 0 - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): return (rd.random() < 0.1) @@ -119,10 +119,10 @@ class Branch(BasicWood): self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Branch.count+=1 self.num_buds_segment=0 - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.01*(1 - self.num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.01*(1 - self.num_buds_segment/self.growth.max_buds_segment)) def create_branch(self): self.num_buds_segment += 1 @@ -151,10 +151,10 @@ class Trunk(BasicWood): if not name: self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Trunk.count+=1 - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): - if (rd.random() > 0.2*(1 - num_buds_segment/self.max_buds_segment)): + if (rd.random() > 0.2*(1 - num_buds_segment/self.growth.max_buds_segment)): return False return True @@ -183,10 +183,10 @@ class NonTrunk(BasicWood): if not name: self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Branch.count+=1 - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.02*(1 - num_buds_segment/self.growth.max_buds_segment)) def create_branch(self): if rd.random()>0.9: @@ -246,12 +246,12 @@ def get_energy_mat(branches, arch): energy_matrix = np.ones((num_branches,num_wires))*np.inf #print(energy_matrix.shape) for branch_id, branch in enumerate(branches): - if branch.has_tied: + if branch.tying.has_tied: continue for wire_id, wire in arch.branch_supports.items(): if wire.num_branch>=1: continue - energy_matrix[branch_id][wire_id] = ed(wire.point,branch.end)/2+ed(wire.point,branch.start)/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age) + energy_matrix[branch_id][wire_id] = ed(wire.point,branch.location.end)/2+ed(wire.point,branch.location.start)/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.info.age) return energy_matrix @@ -261,9 +261,9 @@ def decide_guide(energy_matrix, branches, arch): #print(min_arg) if(energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf) or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 1: return - if not (branches[min_arg[0][0]].has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): + if not (branches[min_arg[0][0]].tying.has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): #print("Imp:",min_arg[0][0], min_arg[0][1], energy_matrix[min_arg[0][0]][min_arg[0][1]]) - branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) + branches[min_arg[0][0]].tying.guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) #trellis_wires.trellis_pts[min_arg[0][1]].num_branches+=1 for j in range(energy_matrix.shape[1]): energy_matrix[min_arg[0][0]][j] = np.inf @@ -273,13 +273,13 @@ def decide_guide(energy_matrix, branches, arch): def tie(lstring): for j,i in enumerate(lstring): if i == 'C' and i[0].type.__class__.__name__ == 'Branch': - if i[0].type.tie_updated == False: + if i[0].type.tying.tie_updated == False: continue curr = i[0] - if i[0].type.guide_points: - #print("tying ", i[0].type.name, i[0].type.guide_target.point) - i[0].type.tie_updated = False - i[0].type.guide_target.add_branch() + if i[0].type.tying.guide_points: + #print("tying ", i[0].type.name, i[0].type.tying.guide_target.point) + i[0].type.tying.tie_updated = False + i[0].type.tying.guide_target.add_branch() lstring, count = i[0].type.tie_lstring(lstring, j) return True @@ -289,9 +289,9 @@ def tie(lstring): def pruning_strategy(lstring): #Remove remnants of cut cut = False for j,i in enumerate(lstring): - if i.name == 'C' and i[0].type.age > 6 and i[0].type.has_tied == False and i[0].type.cut == False: + if i.name == 'C' and i[0].type.info.age > 6 and i[0].type.tying.has_tied == False and i[0].type.info.cut == False: - i[0].type.cut = True + i[0].type.info.cut = True #print("Cutting", i[0].type.name) lstring = cut_using_string_manipulation(j, lstring) return True @@ -300,7 +300,7 @@ def pruning_strategy(lstring): #Remove remnants of cut def StartEach(lstring): global parent_child_dict for i in parent_child_dict[trunk_base.name]: - if i.tie_updated == False: + if i.tying.tie_updated == False: i.tie_update() @@ -313,8 +313,8 @@ def EndEach(lstring): #print(energy_matrix) decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) for branch in parent_child_dict[trunk_base.name]: - branch.update_guide(branch.guide_target) - #print(branch.name, branch.guide_target) + branch.update_guide(branch.tying.guide_target) + #print(branch.name, branch.tying.guide_target) while tie(lstring): pass while pruning_strategy(lstring): @@ -335,7 +335,7 @@ rd.seed(seed_val) # Added by Camp -def create_noisy_circle_curve(radius, noise_factor, num_points=100, seed=None): +def create_noisy_branch_contour(radius, noise_factor, num_points=100, seed=None): if seed is not None: rd.seed(seed) t = np.linspace(0, 2 * np.pi, num_points, endpoint=False) @@ -463,13 +463,13 @@ def make_leaf_guide(): def reset_contour(): - default_curve = create_noisy_circle_curve(1, 0, 30) # Example of a simple default contour + default_curve = create_noisy_branch_contour(1, 0, 30) # Example of a simple default contour nproduce SetContour(default_curve) global profile1, profile2, profile3 -profile1 = create_noisy_circle_curve(1, .2, 30, 23) -profile2 = create_noisy_circle_curve(1, .2, 30) -profile3 = create_noisy_circle_curve(1, .2, 30) +profile1 = create_noisy_branch_contour(1, .2, 30, 23) +profile2 = create_noisy_branch_contour(1, .2, 30) +profile3 = create_noisy_branch_contour(1, .2, 30) # print("Labelling: ", label) # print("Seed ", seed_val) @@ -483,21 +483,21 @@ grow_object(o) : if o == None: produce * else: - if o.length >= o.max_length: - o.age += 1 + if o.length >= o.growth.max_length: + o.info.age += 1 nproduce * else: # Apply color - r, g, b = o.color + r, g, b = o.info.color nproduce SetColor(r, g, b) # set unique color ID smallest_color = [r, g, b].index(min([r, g, b])) - o.color[smallest_color] += 1 + o.info.color[smallest_color] += 1 # Check if it's a trunk or branch, then apply the contour if 'Trunk' in o.name or 'Branch' in o.name: #print("TRUNK BRANCH NAME: " + o.name) - radius = o.thickness + radius = o.growth.thickness noise_factor = 0.8 num_points = 60 nproduce SetContour(o.contour) @@ -507,7 +507,7 @@ grow_object(o) : o.grow_one() if 'Spur' in o.name: - produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) spiked_bud(o.thickness)grow_object(o) + produce I(o.growth.growth_length, o.growth.thickness, o) bud(ParameterSet(type=o, num_buds=0)) spiked_bud(o.growth.thickness)grow_object(o) elif 'Leaf' in o.name: produce L(.1) elif 'Apple' in o.name: @@ -516,7 +516,7 @@ grow_object(o) : #produce [S(.1, .007)Ap] produce [S(.1/2, .09/15)f(.1)&(180)A(.1, .09)] else: - produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) grow_object(o) + produce I(o.growth.growth_length, o.growth.thickness, o) bud(ParameterSet(type=o, num_buds=0)) grow_object(o) #L(.1) bud(t) : @@ -528,7 +528,7 @@ bud(t) : parent_child_dict[t.type.name].append(new_object) #Store new object somewhere t.num_buds+=1 - t.type.num_branches+=1 + t.type.info.num_branches+=1 # set a different cross section for every branch if 'Leaf' not in new_object.name and 'Apple' not in new_object.name: @@ -538,13 +538,13 @@ bud(t) : # set a curve for tertiary branches to follow as they grow if 'NonTrunk' in new_object.name: import time - r, g, b = new_object.color + r, g, b = new_object.info.color seed = rgb_seed(r, g, b) curve = create_bezier_curve(seed=time.time()) - nproduce [SetGuide(curve, new_object.max_length) + nproduce [SetGuide(curve, new_object.growth.max_length) else: nproduce [ - nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*90)grow_object(new_object)GetPos(new_object.end)]bud(t) + nproduce @RGetPos(new_object.location.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*90)grow_object(new_object)GetPos(new_object.location.end)]bud(t) spiked_bud(r): base_height = r * 2 @@ -585,8 +585,8 @@ L(l): produce _(.0025) F(l/10){[SetGuide(curve1, l) _(.001).nF(l, .01)][SetGuide(curve2, l)_(.001).nF(l, .01)]} -I(s,r,o) --> I(s,r+o.thickness_increment, o) -#_(r) --> _(r+o.thickness_increment) +I(s,r,o) --> I(s,r+o.growth.thickness_increment, o) +#_(r) --> _(r+o.growth.thickness_increment) _(r) --> _(r) homomorphism: I(a,r,o) --> F(a,r) diff --git a/examples/Envy_tie_prune_label.lpy b/other_files/legacy/Envy_tie_prune_label.lpy similarity index 80% rename from examples/Envy_tie_prune_label.lpy rename to other_files/legacy/Envy_tie_prune_label.lpy index eb085a0..5975ed2 100644 --- a/examples/Envy_tie_prune_label.lpy +++ b/other_files/legacy/Envy_tie_prune_label.lpy @@ -36,11 +36,11 @@ class Spur(BasicWood): # Keep count of number of leaves to not go over max_leaves self.num_leaves = 0 - self.prunable = False + self.info.prunable = False # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve - self.contour = create_noisy_circle_curve(radius = 2, noise_factor = .2, num_points = 30) + self.contour = create_noisy_branch_contour(radius = 2, noise_factor = .2, num_points = 30) def is_bud_break(self, num_buds_segment): return (rd.random() < 0.1) @@ -72,12 +72,12 @@ class Branch(BasicWood): # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve - self.contour = create_noisy_circle_curve(1, .5, 30) + self.contour = create_noisy_branch_contour(1, .5, 30) def is_bud_break(self, num_break_buds): if num_break_buds >= 1: return False - return (rd.random() < 0.5*(1 - self.num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.5*(1 - self.num_buds_segment/self.growth.max_buds_segment)) def create_branch(self): self.num_buds_segment += 1 @@ -109,10 +109,10 @@ class Trunk(BasicWood): # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve - self.contour = create_noisy_circle_curve(3, 0.05, 30) + self.contour = create_noisy_branch_contour(3, 0.05, 30) def is_bud_break(self, num_buds_segment): - if (rd.random() > 0.1*(1 - num_buds_segment/self.max_buds_segment)): + if (rd.random() > 0.1*(1 - num_buds_segment/self.growth.max_buds_segment)): return False return True @@ -141,14 +141,14 @@ class NonTrunk(BasicWood): if not name: self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Branch.count+=1 - self.prunable = False + self.info.prunable = False # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.5*(1 - num_buds_segment/self.growth.max_buds_segment)) def create_branch(self): if rd.random()>0.3: @@ -166,10 +166,10 @@ growth_length = 0.1 bud_spacing_age = 2 #everything is relative to growth length basicwood_prototypes = {} -basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 4, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,0,0] ) -basicwood_prototypes['branch'] = Branch(tie_axis = (0,1,1), max_length = 2.2, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,150,0] ) -basicwood_prototypes['nontrunk'] = NonTrunk(tie_axis = (0,1,1), max_length = 0.3, growth_length = growth_length/2, thickness = 0.003,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [0, 255, 0] ) -basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.2, thickness = 0.003, growth_length = growth_length/2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0,255,0] ) +basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,0), max_length = 4, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,0,0] ) +basicwood_prototypes['branch'] = Branch(tie_axis = (1, 0, 0), max_length = 2.2, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,150,0] ) +basicwood_prototypes['nontrunk'] = NonTrunk(tie_axis = (1, 0, 0), max_length = 0.3, growth_length = growth_length/2, thickness = 0.003,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [0, 255, 0] ) +basicwood_prototypes['spur'] = Spur(tie_axis = (1, 0, 0), max_length = 0.2, thickness = 0.003, growth_length = growth_length/2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0,255,0] ) #init @@ -191,7 +191,7 @@ def generate_points_v_trellis(): return pts # Return the list of points # points, num_wires spacing wires trunk_wire_pt branch_axis trunk_axis -support = Support(generate_points_v_trellis(), 14 , 1 , None, (0,0,1), None) +support = Support(generate_points_v_trellis(), 14 , 1 , None) num_iteration_tie = 5 num_iteration_prune = 16 ###Tying stuff begins @@ -208,14 +208,14 @@ def get_energy_mat(branches, arch): energy_matrix = np.ones((num_branches, num_wires)) * np.inf # Initialize the energy matrix with infinity for branch_id, branch in enumerate(branches): # Loop through each branch - if branch.has_tied or branch.cut: # If the branch is already tied, skip it + if branch.tying.has_tied or branch.info.cut: # If the branch is already tied, skip it continue for wire_id, wire in arch.branch_supports.items(): # Loop through each wire # print(f"ID {wire_id} num branch {wire.num_branch}") if wire.num_branch >= 1: # If the wire already has a branch, skip it continue # Calculate the energy required to tie the branch to the wire - energy_matrix[branch_id][wire_id] = ed(wire.point, branch.start) / 2 #ed(wire.point, branch.end) / 2 + + energy_matrix[branch_id][wire_id] = ed(wire.point, branch.location.start) / 2 #ed(wire.point, branch.location.end) / 2 + return energy_matrix # Return the energy matrix @@ -225,8 +225,8 @@ def decide_guide(energy_matrix, branches, arch): min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) # Find the minimum energy branch to tie if (energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf) or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 0.25*math.exp(getIterationNb()/105): return # If the minimum energy is too high, stop the process - if not branches[min_arg[0][0]].has_tied and not branches[min_arg[0][0]].cut: # If the branch has not been tied before - branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]] # Set the guide target to the wire + if not branches[min_arg[0][0]].tying.has_tied and not branches[min_arg[0][0]].info.cut: # If the branch has not been tied before + branches[min_arg[0][0]].tying.guide_target = arch.branch_supports[min_arg[0][1]] # Set the guide target to the wire arch.branch_supports[min_arg[0][1]].add_branch() # Increment the number of branches tied to the wire # print(f"Tying {min_arg[0][0]} to wire {min_arg[0][1]}") # Set the energy to tie as infinite (only tie 1 branch per wire) @@ -239,11 +239,11 @@ def decide_guide(energy_matrix, branches, arch): def tie(lstring): for j, i in enumerate(lstring): # Loop through each element in the lstring if i == 'C' and i[0].type.__class__.__name__ == 'Branch': # Check if the element is a branch - if i[0].type.tie_updated == False: # If the branch is not updated, skip it + if i[0].type.tying.tie_updated == False: # If the branch is not updated, skip it continue curr = i[0] # Get the current branch - if i[0].type.guide_points: # If the branch has guide points - i[0].type.tie_updated = False # Set the tie_updated flag to False + if i[0].type.tying.guide_points: # If the branch has guide points + i[0].type.tying.tie_updated = False # Set the tie_updated flag to False #i[0].type.guide_target.add_branch() # Add the branch to the guide target lstring, count = i[0].type.tie_lstring(lstring, j) # Tie the branch @@ -254,8 +254,8 @@ def tie(lstring): def pruning_strategy(lstring): # Remove remnants of cut cut = False # Initialize the cut flag for j, i in enumerate(lstring): # Loop through each element in the lstring - if i.name == 'C' and i[0].type.age > 6 and i[0].type.has_tied == False and i[0].type.cut == False and i[0].type.prunable and i[0].type.guide_target==-1: - i[0].type.cut = True # Set the cut flag to True + if i.name == 'C' and i[0].type.info.age > 6 and i[0].type.tying.has_tied == False and i[0].type.info.cut == False and i[0].type.info.prunable and i[0].type.tying.guide_target==-1: + i[0].type.info.cut = True # Set the cut flag to True lstring = cut_from(j, lstring) # Cut the branch using string manipulation #TODO: Remove branch from parent_child dict and wire #if i[0].type.guide_target!=-1: @@ -267,7 +267,7 @@ def pruning_strategy(lstring): # Remove remnants of cut def StartEach(lstring): global parent_child_dict for i in parent_child_dict[trunk_base.name]: # Loop through each child of the trunk base - if i.tie_updated == False: # If the branch is not updated + if i.tying.tie_updated == False: # If the branch is not updated i.tie_update() # Update the branch # Function to end each iteration @@ -278,7 +278,7 @@ def EndEach(lstring): energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) # Get the energy matrix decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) # Decide which branches to guide for branch in parent_child_dict[trunk_base.name]: # Loop through each branch - branch.update_guide(branch.guide_target) # Update the guide target for the branch + branch.update_guide(branch.tying.guide_target) # Update the guide target for the branch while tie(lstring): # Tie branches until no more can be tied pass if (getIterationNb() + 1) % num_iteration_prune == 0: @@ -304,12 +304,12 @@ rd.seed(seed_val) # Used to set the contour back to a circle after it has been changed for a branch def reset_contour(): - default_curve = create_noisy_circle_curve(1, 0, 30) # Example of a simple default contour + default_curve = create_noisy_branch_contour(1, 0, 30) # Example of a simple default contour nproduce SetContour(default_curve) curve = create_bezier_curve(x_range = (-1, 1), y_range = (-0.15, 0.15), z_range = (0, 10), seed_val=time.time()) -Axiom: Attractors(support)SetGuide(curve, trunk_base.max_length)[grow_object(trunk_base)] +Axiom: Attractors(support)SetGuide(curve, trunk_base.growth.max_length)[grow_object(trunk_base)] derivation length: 128 production: #Decide whether branch internode vs trunk internode need to be the same size. @@ -317,12 +317,12 @@ grow_object(o) : if o == None: produce * else: - if o.length >= o.max_length: - o.age += 1 + if o.length >= o.growth.max_length: + o.info.age += 1 nproduce * else: # Get object's usual color and apply it - r, g, b = o.color + r, g, b = o.info.color nproduce SetColor(r, g, b) # This sets unique color IDs @@ -342,11 +342,11 @@ grow_object(o) : if 'Spur' in o.name: # note that the production of the buds is here with 'spiked_bud(o.thickness)' - produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0))grow_object(o) #spiked_bud(o.thickness)grow_object(o) + produce I(o.growth.growth_length, o.growth.thickness, o) bud(ParameterSet(type=o, num_buds=0))grow_object(o) #spiked_bud(o.growth.thickness)grow_object(o) else: # If o is a Trunk, Branch, or NonTrunk, simply produce the internodes - nproduce I(o.growth_length, o.thickness, o) - if np.isclose(o.age%bud_spacing_age,0, atol = 0.01): + nproduce I(o.growth.growth_length, o.growth.thickness, o) + if np.isclose(o.info.age%bud_spacing_age,0, atol = 0.01): nproduce bud(ParameterSet(type=o, num_buds=0)) produce grow_object(o) @@ -361,20 +361,20 @@ bud(t) : parent_child_dict[t.type.name].append(new_object) #Store new object somewhere t.num_buds+=1 - t.type.num_branches+=1 + t.type.info.num_branches+=1 # Set a curve for tertiary branches to follow as they grow if 'NonTrunk' in new_object.name: import time curve = create_bezier_curve(x_range = (-.5,.5), y_range = (-.5,.5), z_range = (-1,1), seed_val=time.time()) - nproduce [SetGuide(curve, new_object.max_length) + nproduce [SetGuide(curve, new_object.growth.max_length) elif 'Spur' in new_object.name: import time curve = create_bezier_curve(x_range = (-.2,.2), y_range = (-.2,.2), z_range = (-1,1), seed_val=time.time()) - nproduce [SetGuide(curve, new_object.max_length) + nproduce [SetGuide(curve, new_object.growth.max_length) else: nproduce [ - nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.end)]bud(ParameterSet(type=t.type, num_buds=t.num_buds)) + nproduce @RGetPos(new_object.location.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.location.end)]bud(ParameterSet(type=t.type, num_buds=t.num_buds)) # Simple set of productions to build apple bud. This bud is @@ -397,8 +397,8 @@ A(bh, r): produce nF(bh, .01, r, base_curve) ^(180) nF(bh/5, .1, r, top_curve)^(180)#S(bh/2,r/15) -I(s,r,o) --> I(s,r+o.thickness_increment, o) -#_(r) --> _(r+o.thickness_increment) +I(s,r,o) --> I(s,r+o.growth.thickness_increment, o) +#_(r) --> _(r+o.growth.thickness_increment) _(r) --> _(r) homomorphism: I(a,r,o) --> F(a,r) diff --git a/other_files/legacy/UFO_tie_prune_label.lpy b/other_files/legacy/UFO_tie_prune_label.lpy new file mode 100644 index 0000000..99206be --- /dev/null +++ b/other_files/legacy/UFO_tie_prune_label.lpy @@ -0,0 +1,321 @@ +""" +Tying, Pruning and lablelling UFO architecture trees +""" +import sys +sys.path.append('../') +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +import numpy as np +import random as rd +import copy +import gc +import time +from helper import * + +# Used to set the contour back to a circle after it has been changed for a branch +def reset_contour(): + default_curve = create_noisy_branch_contour(1, 0, 30) # Example of a simple default contour + nproduce SetContour(default_curve) + +class Spur(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + return (rd.random() < 0.1) + + def create_branch(self): + return None + + +class TertiaryBranch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= 2: + return False + if (rd.random() < 0.005*self.growth.growth_length*(1 - self.num_buds/self.growth.max_buds_segment)): + self.num_buds +=1 + return True + + def create_branch(self): + if rd.random()>0.8: + new_ob = Branch(copy_from = self.prototype_dict['side_branch']) + else: + new_ob = Spur(copy_from = self.prototype_dict['spur']) + return new_ob + +class Branch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= 2: + return False + if (rd.random() < 0.2*(1 - self.num_buds/self.growth.max_buds_segment)): + + return True + + def create_branch(self): + try: + if rd.random()>0.9: + new_ob = TertiaryBranch(copy_from = self.prototype_dict['side_branch']) + else: + new_ob = Spur(copy_from = self.prototype_dict['spur']) + except: + return None + return new_ob + + +class Trunk(TreeBranch): + """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + if (rd.random() > 0.05*self.length/self.growth.max_length*(1 - self.num_buds/self.growth.max_buds_segment)): + return False + self.num_buds+=1 + return True + + def create_branch(self): + if rd.random() > 0.1: + return Branch(copy_from = self.prototype_dict['branch']) + + + +# growth_length = 0.1 +basicwood_prototypes = {} + +# Create configs for cleaner prototype setup +spur_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=(1, 0, 0), + max_length=0.1, + thickness=0.003, + growth_length=0.05, + thickness_increment=0., + color=[0, 255, 0] +) + +side_branch_config = BasicWoodConfig( + max_buds_segment=40, + tie_axis=(0, 0, 1), + max_length=0.25, + thickness=0.003, + growth_length=0.05, + thickness_increment=0.00001, + color=[0, 255, 0] +) + +trunk_config = BasicWoodConfig( + max_buds_segment=60, + tie_axis=(1, 0, 0), + max_length=3, + thickness=0.02, + thickness_increment=0.00001, + growth_length=0.1, + color=[255, 0, 0] +) + +branch_config = BasicWoodConfig( + max_buds_segment=140, + tie_axis=(0, 0, 1), + max_length=2.5, + thickness=0.01, + thickness_increment=0.00001, + growth_length=0.1, + color=[255, 150, 0] +) + +# Setup prototypes using configs +basicwood_prototypes['spur'] = Spur(config=spur_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['side_branch'] = TertiaryBranch(config=side_branch_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['trunk'] = Trunk(config=trunk_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['branch'] = Branch(config=branch_config, prototype_dict=basicwood_prototypes) + +#init +trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) +time_count = 0 + +def generate_points_ufo(): + x = np.arange(0.65, 3, 0.3).astype(float) + z = np.full((x.shape[0],), 1.4).astype(float) + y = np.full((x.shape[0],), 0).astype(float) + pts = [] + id = 0 + for i in range(x.shape[0]): + pts.append((x[i], y[i], z[i])) + id+=1 + + return pts + + + +support = Support(generate_points_ufo(), 7 , 1 , (0.6,0,0.4)) +num_iteration_tie = 8 +num_iteration_prune = 16 +bud_spacing_age = 2 +trunk_base.tying.guide_target = support.trunk_wire +###Tying stuff begins + +def ed(a,b): + return (a[0]-b[0])**2+(a[1]-b[1])**2+(a[2]-b[2])**2 + +def get_energy_mat(branches, arch): + num_branches = len(branches) + num_wires = len(list(arch.branch_supports.values())) + energy_matrix = np.ones((num_branches,num_wires))*np.inf + for branch_id, branch in enumerate(branches): + if branch.tying.has_tied: + continue + for wire_id, wire in arch.branch_supports.items(): + if wire.num_branch>=1: + continue + energy_matrix[branch_id][wire_id] = ed(wire.point, branch.location.start)/2+ed(wire.point,branch.location.end)/2#,+v.num_branches*10+branch.bend_energy(deflection, curr_branch.info.age) + return energy_matrix + +def decide_guide(energy_matrix, branches, arch): + for i in range(energy_matrix.shape[0]): + min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) + if energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 1 : + return + if not (branches[min_arg[0][0]].tying.has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): + branches[min_arg[0][0]].tying.guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) + arch.branch_supports[min_arg[0][1]].add_branch() # Increment the number of branches tied to the wire + + #trellis_wires.trellis_pts[min_arg[0][1]].num_branches+=1 + for j in range(energy_matrix.shape[1]): + energy_matrix[min_arg[0][0]][j] = np.inf + for j in range(energy_matrix.shape[0]): + energy_matrix[j][min_arg[0][1]] = np.inf + +def tie(lstring): + for j,i in enumerate(lstring): + if (i == 'C' and i[0].type.__class__.__name__ == 'Branch') or i == 'T' : + if i[0].type.tying.tie_updated == False: + continue + curr = i[0] + if i[0].type.tying.guide_points: + i[0].type.tying.tie_updated = False + i[0].type.tying.guide_target.add_branch() + lstring, count = i[0].type.tie_lstring(lstring, j) + + return True + return False + + +def StartEach(lstring): + global parent_child_dict, support, trunk_base + if support.trunk_wire and trunk_base.tying.tie_updated == False: + trunk_base.tie_update() + + for i in parent_child_dict[trunk_base.name]: + if i.tying.tie_updated == False: + i.tie_update() + + +def EndEach(lstring): + global parent_child_dict, support, num_iteration_tie + + tied = False + if (getIterationNb()+1)%num_iteration_tie == 0: + + if support.trunk_wire : + trunk_base.update_guide(trunk_base.tying.guide_target) #Tie trunk one iteration before branches + energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) + decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) + for branch in parent_child_dict[trunk_base.name]: + branch.update_guide(branch.tying.guide_target) + while tie(lstring): + pass + if (getIterationNb() + 1) % num_iteration_prune == 0: + while pruning_strategy(lstring): # Prune branches until no more can be pruned + pass + return lstring + +def pruning_strategy(lstring): #Remove remnants of cut + cut = False + for j,i in enumerate(lstring): + if i.name == 'C' and i[0].type.info.age > 8 and i[0].type.tying.has_tied == False and i[0].type.info.cut == False: + i[0].type.info.cut = True + lstring = cut_from(j, lstring) + return True + return False + +parent_child_dict = {} +parent_child_dict[trunk_base.name] = [] +label = True +#Tie trunk +module Attractors +module grow_object +module bud +module branch +module C +curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) +Axiom: Attractors(support)SetGuide(curve, trunk_base.growth.max_length)[@GcGetPos(trunk_base.location.start)T(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.location.end)] +derivation length: 160 + +production: +#Decide whether branch internode vs trunk internode need to be the same size. +grow_object(o) : + if o == None: + produce * + if o.length >= o.growth.max_length: + nproduce * + else: + nproduce SetContour(o.contour) + o.grow_one() + if label: + r, g, b = o.info.color + nproduce SetColor(r,g,b) + if 'Spur' in o.name: + produce I(o.growth.growth_length, o.growth.thickness, o)bud(ParameterSet(type = o, num_buds = 0))@O(o.growth.thickness*1.2)grow_object(o) + else: + nproduce I(o.growth.growth_length, o.growth.thickness, o) + if np.isclose(o.info.age%bud_spacing_age,0, atol = 0.01): + nproduce bud(ParameterSet(type = o, num_buds = 0)) + produce grow_object(o) + #produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))grow_object(o) + +bud(t) : + if t.type.is_bud_break(t.num_buds): + new_object = t.type.create_branch() + if new_object == None: + produce * + parent_child_dict[new_object.name] = [] + parent_child_dict[t.type.name].append(new_object) + #Store new object somewhere + t.num_buds+=1 + t.type.info.num_branches+=1 + + if 'TertiaryBranch' in new_object.name: + import time + curve = create_bezier_curve(x_range=(-.5, .5), y_range=(-.5, .5), z_range=(-1, 1), seed_val=time.time()) + nproduce[@GcSetGuide(curve, new_object.growth.max_length) + elif 'Spur' in new_object.name: + import time + curve = create_bezier_curve(x_range=(-.2, .2), y_range=(-.2, .2), z_range=(-1, 1), seed_val=time.time()) + nproduce[@GcSetGuide(curve, new_object.growth.max_length) + else: + nproduce [ + nproduce @RGetPos(new_object.location.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.location.end)@Ge]bud(t) + + +I(s,r,o) --> I(s,r+o.growth.thickness_increment, o) +_(r) --> _(r+o.growth.thickness_increment) + +homomorphism: + +I(a,r,o) --> F(a,r) +S(a,r,o) --> F(a,r) + +production: +Attractors(support): + pttodisplay = support.attractor_grid.get_enabled_points() + if len(pttodisplay) > 0: + produce [,(3) @g(PointSet(pttodisplay,width=10))] diff --git a/examples/static_envy.lpy b/other_files/legacy/static_envy.lpy similarity index 100% rename from examples/static_envy.lpy rename to other_files/legacy/static_envy.lpy diff --git a/examples/static_ufo.lpy b/other_files/legacy/static_ufo.lpy similarity index 100% rename from examples/static_ufo.lpy rename to other_files/legacy/static_ufo.lpy diff --git a/simulation_base.py b/simulation_base.py new file mode 100644 index 0000000..e423aff --- /dev/null +++ b/simulation_base.py @@ -0,0 +1,329 @@ +""" +Base simulation module for L-System tree training and trellis systems. + +This module provides common functionality for tree architecture simulations including: +- Energy-based branch-to-wire optimization +- Tying operations for attaching branches to trellis wires +- Pruning strategies for untied branches + +Architecture-specific implementations (Envy, UFO, etc.) should inherit from this base +and implement architecture-specific methods like point generation. +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +import numpy as np +from helper import cut_from + + +@dataclass +class SimulationConfig(ABC): + """Base configuration class for tree training simulations. + + Architecture-specific configs should inherit from this and add their own parameters. + Common parameters across all architectures are defined here. + """ + + # Tying and Pruning Intervals + num_iteration_tie: int = 5 + num_iteration_prune: int = 16 + + # Label Options + semantic_label: bool = False + instance_label: bool = True + per_cylinder_label: bool = False + # Support Structure + support_num_wires: int = 14 + support_spacing_wires: int = 1 + support_trunk_wire_point: tuple = None + + # Energy and Tying Parameters + energy_distance_weight: float = 0.5 # Weight for distance in energy calculation + energy_threshold: float = 1.0 # Maximum energy threshold for tying + + # Pruning Parameters + pruning_age_threshold: int = 6 # Age threshold for pruning untied branches + + # L-System Parameters + derivation_length: int = 128 # Number of derivation steps + use_generalized_cylinder: bool = False # Whether to wrap new branches in @Gc/@Ge blocks + + # Growth Parameters + tolerance: float = 1e-5 # Tolerance for comparison between floats + + # Visualization Parameters + attractor_point_width: int = 10 # Width of attractor points in visualization + + def __post_init__(self): + """Make sure only one labeling option is enabled at a time.""" + label_options = [ + self.semantic_label, + self.instance_label, + self.per_cylinder_label + ] + if sum(label_options) > 1: + raise ValueError( + "Only one of semantic_label, instance_label, or per_cylinder_label " + "can be True at a time." + ) + + +class TreeSimulationBase(ABC): + """ + Base class for tree architecture simulations with trellis training. + + This class provides common algorithms for: + - Energy-based optimization for branch-to-wire assignment + - Greedy assignment of branches to wires + - Pruning operations for untied branches + - Tying operations to modify L-System strings + + Architecture-specific implementations should: + 1. Inherit from this class + 2. Implement generate_points() for their specific trellis layout + 3. Optionally override methods if custom behavior is needed + """ + + def __init__(self, config: SimulationConfig): + """ + Initialize the simulation with a configuration object. + + Args: + config: SimulationConfig instance with parameters for the simulation + """ + self.config = config + + @abstractmethod + def generate_points(self): + """ + Generate 3D points for the trellis wire structure. + + This method must be implemented by architecture-specific subclasses + to define the layout of trellis wires (V-trellis, UFO, etc.). + + Returns: + list: List of (x, y, z) tuples representing wire attachment points + """ + pass + + def get_energy_mat(self, branches, arch): + """ + Calculate the energy matrix for optimal branch-to-wire assignment. + + This function computes an energy cost matrix where each entry represents the + "cost" of assigning a specific branch to a specific wire in the trellis system. + The energy is based on the Euclidean distance from wire attachment points to + both the start and end points of each branch, weighted by the simulation's + distance weight parameter. + + The algorithm uses a greedy optimization approach where branches are assigned + to the lowest-energy available wire that hasn't reached capacity. + + Args: + branches: List of branch objects to be assigned to wires + arch: Support architecture object containing wire information + + Returns: + numpy.ndarray: Energy matrix of shape (num_branches, num_wires) where + matrix[i][j] is the energy cost of assigning branch i to wire j. + Untied branches and occupied wires have infinite energy (np.inf). + """ + num_branches = len(branches) + num_wires = len(arch.branch_supports) + + # Initialize energy matrix with infinite values (impossible assignments) + energy_matrix = np.full((num_branches, num_wires), np.inf) + + # Calculate energy costs for all valid branch-wire combinations + for branch_idx, branch in enumerate(branches): + # Skip branches that are already tied + if branch.tying.has_tied: + continue + + for wire_id, wire in arch.branch_supports.items(): + # Skip wires that already have a branch attached + if wire.num_branch >= 1: + continue + + # Calculate weighted distance energy for this branch-wire pair + # Energy considers distance from wire to both branch endpoints + wire_point = np.array(wire.point) + branch_start = np.array(branch.location.start) + branch_end = np.array(branch.location.end) + + start_distance_energy = np.sum((wire_point - branch_start) ** 2) + end_distance_energy = np.sum((wire_point - branch_end) ** 2) + + total_energy = (start_distance_energy + end_distance_energy) * self.config.energy_distance_weight + + energy_matrix[branch_idx, wire_id] = total_energy + + return energy_matrix + + def decide_guide(self, energy_matrix, branches, arch): + """ + Perform greedy assignment of branches to wires based on energy matrix. + + This function implements a greedy optimization algorithm that iteratively assigns + the branch-wire pair with the lowest energy cost. Once a branch is assigned to + a wire, both that branch and wire are marked as unavailable (infinite energy) + to prevent further assignments. + + The algorithm continues until no valid assignments remain (all remaining energies + are infinite or above the threshold). + + Args: + energy_matrix: numpy.ndarray of shape (num_branches, num_wires) with energy costs + branches: List of branch objects to be assigned + arch: Support architecture containing wire information + + Returns: + None: Modifies branches and arch in-place with new assignments + """ + num_branches, num_wires = energy_matrix.shape + + # Early return if no branches or wires to assign + if num_branches == 0 or num_wires == 0: + return + + # Continue making assignments until no valid ones remain + while True: + # Find the minimum energy value and its position + min_energy_indices = np.argwhere(energy_matrix == np.min(energy_matrix)) + + # If no valid indices found or matrix is empty, stop + if len(min_energy_indices) == 0: + break + + # Get the first (and typically only) minimum energy position + branch_idx, wire_id = min_energy_indices[0] + min_energy = energy_matrix[branch_idx, wire_id] + + # Stop if minimum energy is infinite (no valid assignments) or above threshold + if np.isinf(min_energy) or min_energy > self.config.energy_threshold: + break + + # Get the branch and wire objects + branch = branches[branch_idx] + wire = arch.branch_supports[wire_id] + + # Skip if branch is already tied (defensive check) + if branch.tying.has_tied: + # Mark this assignment as invalid and continue + energy_matrix[branch_idx, wire_id] = np.inf + continue + + # Perform the assignment + branch.tying.guide_target = wire + wire.add_branch() + + # Mark branch and wire as unavailable for future assignments + # Set entire row (branch) to infinity - this branch can't be assigned again + energy_matrix[branch_idx, :] = np.inf + # Set entire column (wire) to infinity - this wire can't accept more branches + energy_matrix[:, wire_id] = np.inf + + def prune(self, lstring): + """ + Prune old branches that exceed the age threshold and haven't been tied to wires. + + This function implements the pruning strategy for the tree training simulation. + It identifies branches that have grown too old (exceeding the pruning age threshold) + but haven't been successfully tied to trellis wires. Such branches are considered + unproductive and are removed from the L-System to encourage new growth. + + The pruning criteria are: + 1. Branch age exceeds the configured pruning threshold + 2. Branch has not been tied to any trellis wire + 3. Branch has not already been marked for cutting + 4. Branch is prunable (respects the prunable flag) + + When a branch meets all criteria, it is: + - Marked as cut (to prevent re-processing) + - Removed from the L-System string using cut_from() + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a branch was pruned, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + pruning a single branch. It should be called repeatedly (e.g., in a while loop) + until no more pruning operations are possible. The cut_from() function handles + the actual removal of the branch and any dependent substructures from the string. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module (represents a branch) + if symbol.name == 'WoodStart': + branch = symbol[0].type + + # Check pruning criteria + age_exceeds_threshold = branch.info.age > self.config.pruning_age_threshold + not_tied_to_wire = not branch.tying.has_tied + not_already_cut = not branch.info.cut + is_prunable = branch.info.prunable + + # Prune if all criteria are met + if age_exceeds_threshold and not_tied_to_wire and not_already_cut and is_prunable: + # Mark branch as cut to prevent re-processing + branch.info.cut = True + + # Remove the branch from the L-System string + lstring = cut_from(position, lstring) + + return True + + return False + + def tie(self, lstring): + """ + Perform tying operation on eligible branches in the L-System string. + + This function searches through the L-System string for 'WoodStart' modules that + represent branches ready for tying to trellis wires. It identifies branches that: + 1. Have tying properties (tying attribute exists) + 2. Have a defined tie axis (tie_axis is not None) + 3. Have not been tied yet (tie_updated is False) + 4. Have guide points available for wire attachment + + When an eligible branch is found, it performs the tying operation by: + - Marking the branch as tied (tie_updated = False) + - Adding the branch to the target wire + - Calling the branch's tie_lstring method to modify the L-System string + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a tying operation was performed, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + tying a single branch. It should be called repeatedly (e.g., in a while loop) + until no more tying operations are possible. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module with tying capabilities + if (symbol == 'WoodStart' and + hasattr(symbol[0].type, 'tying') and + getattr(symbol[0].type.tying, 'tie_axis', None) is not None): + + branch = symbol[0].type + + # Skip branches that have already been processed for tying + if not branch.tying.tie_updated: + continue + + # Check if branch has guide points for wire attachment + if branch.tying.guide_points: + # Perform the tying operation + branch.tying.tie_updated = False + + # Update the L-System string with tying modifications + lstring, modifications_count = branch.tie_lstring(lstring, position) + + return True + + return False diff --git a/stochastic_tree.py b/stochastic_tree.py index 95105a2..835fecd 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -2,35 +2,109 @@ Defines the abstract class BasicWood, class Wire and class Support. """ -from abc import abstractmethod +from abc import ABC, abstractmethod from openalea.plantgl.all import * import copy import numpy as np from openalea.plantgl.scenegraph.cspline import CSpline import random as rd - +from helper import create_noisy_branch_contour import collections +from dataclasses import dataclass +from typing import Tuple + eps = 1e-6 -from abc import ABC, abstractmethod -# class Tree(): -# #branch_dict = {} -# trunk_dict = {} -# """ This class will have all the parameters required to grow the tree, i.e. the transition -# prob, max trunk length, max branch length etc. Each tree will have its own children branch and trunk classes """ -# def __init__(self): -# self.trunk_num_buds_segment = 5 -# self.branch_num_buds_segment = 5 -# self.trunk_bud_break_prob = 0.5 -# self.branch_bud_break_prob = 0.5 -# self.num_branches = 0 -# self.num_trunks = 0 - - -# # BRANCH AND TRUNK SUBCLASS OF WOOD +@dataclass +class LocationState: + """Location tracking for a wood object: start point, end point, and last tie location.""" + start: any = None # Vector3 + end: any = None # Vector3 + last_tie_location: any = None # Vector3 + + def __post_init__(self): + """Initialize Vector3 points if not provided.""" + if self.start is None: + self.start = Vector3(0, 0, 0) + if self.end is None: + self.end = Vector3(0, 0, 0) + if self.last_tie_location is None: + self.last_tie_location = Vector3(0, 0, 0) -class BasicWood(ABC): +@dataclass +class TyingState: + """Tying and guiding state for a wood object.""" + has_tied: bool = False + guide_points: list = None # List of (x,y,z) tuples for spline control points + guide_target: any = -1 # Wire object or -1 (no target) + tie_axis: tuple = None # Direction vector for the wire axis + tie_updated: bool = False + + def __post_init__(self): + """Initialize guide_points as empty list if not provided.""" + if self.guide_points is None: + self.guide_points = [] +@dataclass +class GrowthState: + """Growth parameters for a wood object.""" + max_buds_segment: int = 5 # Total cumulative buds allowed across the entire branch segment (not per node) + thickness: float = 0.1 + thickness_increment: float = 0.01 + growth_length: float = 1.0 + cylinder_length: float = 0.1 + max_length: float = 7.0 + +@dataclass +class InfoState: + """Information/metadata for a wood object.""" + age: int = 0 + cut: bool = False + prunable: bool = True + order: int = 0 + num_branches: int = 0 + color: tuple = (0, 0, 0) # RGB tuple for visualization + material: int = 0 + branch_dict: any = None # collections.deque + + def __post_init__(self): + """Initialize branch_dict if not provided.""" + if self.branch_dict is None: + self.branch_dict = collections.deque() + +@dataclass +class BasicWoodConfig: + """Configuration parameters for BasicWood initialization.""" + copy_from: any = None + max_buds_segment: int = 5 # Total cumulative buds allowed across the entire branch segment (not per node) + thickness: float = 0.1 + thickness_increment: float = 0.01 + growth_length: float = 1.0 + cylinder_length: float = 0.1 # Length of each individual cylinder + max_length: float = 7.0 + tie_axis: tuple = None + order: int = 0 + color: int = 0 + material: int = 0 + prunable: bool = True + name: str = None + bud_spacing_age: int = 2 # Age interval for bud creation + + # Curve parameters for L-System growth guides + curve_x_range: tuple = (-0.5, 0.5) # X bounds for Bezier curve control points + curve_y_range: tuple = (-0.5, 0.5) # Y bounds for Bezier curve control points + curve_z_range: tuple = (-1, 1) # Z bounds for Bezier curve control points + + def __post_init__(self): + """Validate geometric parameters for consistent growth behavior.""" + if self.growth_length is not None and self.cylinder_length is not None: + if self.growth_length < self.cylinder_length: + raise ValueError( + "BasicWoodConfig.growth_length must be >= cylinder_length " + f"(got {self.growth_length} < {self.cylinder_length})" + ) + +class BasicWood(ABC): @staticmethod def clone(obj): try: @@ -38,41 +112,62 @@ def clone(obj): except copy.Error: raise copy.Error(f'Not able to copy {obj}') from None - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: list = (0,1,1), order: int = 0, color: int = 0, material = 0, name: str = None):#,\ - #bud_break_prob_func: "Function" = lambda x,y: rd.random()): - + def __init__(self, config=None, copy_from=None, **kwargs): + + # Validate parameters + if copy_from is None and config is None: + raise ValueError("Either 'config' or 'copy_from' must be provided") + + # Handle config-based initialization + if config is not None and isinstance(config, BasicWoodConfig): + # Use config values + copy_from = config.copy_from if copy_from is None else copy_from + max_buds_segment = config.max_buds_segment + thickness = config.thickness + thickness_increment = config.thickness_increment + growth_length = config.growth_length + max_length = config.max_length + tie_axis = config.tie_axis + order = config.order + color = config.color + material = config.material + prunable = config.prunable + name = config.name + bud_spacing_age = config.bud_spacing_age + curve_x_range = config.curve_x_range + curve_y_range = config.curve_y_range + curve_z_range = config.curve_z_range + cylinder_length = config.cylinder_length + elif copy_from is None: + raise ValueError("config must be provided when copy_from is None") + #Location variables if copy_from: self.__copy_constructor__(copy_from) return - self.start = Vector3(0,0,0) - self.end = Vector3(0,0,0) + self.location = LocationState() #Tying variables - self.last_tie_location = Vector3(0,0,0) - self.has_tied = False - self.guide_points = [] + self.tying = TyingState(tie_axis=tie_axis) self.current_tied = False - self.guide_target = -1#Vector3(0,0,0) - self.tie_axis = tie_axis - self.tie_updated = False #Information Variables - self.__length = 0 - self.age = 0 - self.cut = False - self.prunable = True - self.order = order - self.num_branches = 0 - self.branch_dict = collections.deque() - self.color = color - self.material = material + self.info = InfoState(order=order, color=color, material=material, prunable=prunable) + self.__length = 0 #Growth Variables - self.max_buds_segment = max_buds_segment - self.thickness = thickness - self.thickness_increment = thickness_increment - self.growth_length = growth_length - self.max_length = max_length + self.growth = GrowthState( + max_buds_segment=max_buds_segment, + thickness=thickness, + thickness_increment=thickness_increment, + growth_length=growth_length, + cylinder_length=cylinder_length, + max_length=max_length + ) + # Bud spacing for L-System rules + self.bud_spacing_age = bud_spacing_age + + # Curve parameters for L-System growth guides + self.curve_x_range = curve_x_range + self.curve_y_range = curve_y_range + self.curve_z_range = curve_z_range @@ -81,7 +176,7 @@ def __copy_constructor__(self, copy_from): for k,v in update_dict.items(): setattr(self, k, v) #self.__dict__.update(update_dict) - + @abstractmethod def is_bud_break(self) -> bool: """This method defines if a bud will break or not -> returns true for yes, false for not. Input can be any variables""" @@ -92,6 +187,16 @@ def is_bud_break(self) -> bool: # if prob_break > self.bud_break_prob: # return True # return False + + @abstractmethod + def pre_bud_rule(self) -> str: + """This method can define any internal changes happening to the properties of the class, such as reduction in thickness increment etc.""" + pass + + @abstractmethod + def post_bud_rule(self) -> str: + """This method can define any internal changes happening to the properties of the class, such as reduction in thickness increment etc.""" + pass @abstractmethod def grow(self) -> None: @@ -103,15 +208,15 @@ def length(self): @length.setter def length(self, length): - self.__length = min(length, self.max_length) + self.__length = min(length, self.growth.max_length) def grow_one(self): - self.age+=1 - self.length+=self.growth_length + self.info.age+=1 + self.length+=self.growth.growth_length self.grow() @abstractmethod - def create_branch(self) -> "BasicWood Object": + def create_branch(self): """Returns how a new order branch when bud break happens will look like if a bud break happens""" pass #new_object = BasicWood.clone(self.branch_object) @@ -120,37 +225,76 @@ def create_branch(self) -> "BasicWood Object": # self.max_length/2, self.tie_axis, self.bud_break_max_length/2, self.order+1, self.bud_break_prob_func) def update_guide(self, guide_target): - curve = [] - self.guide_target = guide_target - if self.guide_target == -1: + """Compute and append guide control points for this wood object. + + Args: + guide_target: Wire object (with .point attribute) or None/-1 (no-op). + + Notes: + - If infeasible (tie point cannot be reached), silently returns. + - Appends control points incrementally to self.tying.guide_points. + - Uses self.location.start as base if not yet tied; self.location.last_tie_location otherwise. + """ + self.tying.guide_target = guide_target + if guide_target is None or guide_target == -1: return - if self.has_tied == False: - curve, i_target = self.get_control_points(self.guide_target.point, self.start , self.end, self.tie_axis) - else: - curve, i_target= self.get_control_points(self.guide_target.point, self.last_tie_location , self.end, self.tie_axis) - if i_target: - self.guide_points.extend(curve) - #self.last_tie_location = copy.deepcopy(Vector3(i_target)) #Replaced by updating location at StartEach + + # Select base point: use last tie location if already tied, otherwise start + base_point = self.location.last_tie_location if self.tying.has_tied else self.location.start + + # Compute control points and tie point in one call + curve, tie_point = self.get_control_points( + guide_target.point, base_point, self.location.end, self.tying.tie_axis + ) + + # Append only if feasible (tie_point is not None) + if tie_point is not None and curve: + self.tying.guide_points.extend(curve) + # Note: last_tie_location updated at StartEach hook, not here def tie_lstring(self, lstring, index): - spline = CSpline(self.guide_points) - if str(spline.curve()) == "nan": - raise ValueError("CURVE IS NAN", self.guide_points) - remove_count = 0 - if not self.has_tied: - if lstring[index+1].name in ['&','/','SetGuide']: - del(lstring[index+1]) - remove_count+=1 - self.has_tied = True - if lstring[index+1].name in ['&','/','SetGuide']: - del(lstring[index+1]) - remove_count+=1 - lstring.insertAt(index+1, 'SetGuide({}, {})'.format(spline.curve(stride_factor = 100), self.length)) - return lstring,remove_count + """Insert a SetGuide(...) after position `index` in `lstring`. + + - Removes any immediate following tokens whose .name is in ('&','/','SetGuide'). + - Builds a CSpline from `self.tying.guide_points` and inserts the curve string and length. + Returns (lstring, removed_count). + """ + # Nothing to do if we don't have guide points + if not self.tying.guide_points: + return lstring, 0 + # Build spline and get curve representation (may raise) + try: + spline = CSpline(self.tying.guide_points) + curve_repr = spline.curve(stride_factor=100) + except Exception as exc: + raise ValueError("Invalid spline from guide_points") from exc + + # Defensive check for 'nan' in the curve representation (preserve original check intent) + if "nan" in str(curve_repr): + raise ValueError("Curve is NaN", self.tying.guide_points) + + # Remove any immediate tokens after index that match the removal set + removal_names = {"&", "/", "SetGuide"} + insert_pos = index + 1 + removed_count = 0 + + # Remove while the next token exists and matches + while insert_pos < len(lstring) and getattr(lstring[insert_pos], "name", None) in removal_names: + del lstring[insert_pos] + removed_count += 1 + + # Mark tied (if not already) + if not self.tying.has_tied: + self.tying.has_tied = True + + # Insert the new SetGuide token at the computed insert position + lstring.insertAt(insert_pos, f"SetGuide({curve_repr}, {self.length})") + + return lstring, removed_count def tie_update(self): - self.last_tie_location = copy.deepcopy(self.end) - self.tie_updated = True + self.location.last_tie_location = copy.deepcopy(self.location.end) + self.tying.tie_updated = True def deflection_at_x(self,d, x, L): """d is the max deflection, x is the current location we need deflection on and L is the total length""" @@ -159,70 +303,171 @@ def deflection_at_x(self,d, x, L): def get_control_points(self, target, start, current, tie_axis): - pts = [] - Lcurve = np.sqrt((start[0]-current[0])**2 + (current[1]-start[1])**2 + (current[2]-start[2])**2) - if Lcurve**2 - (target[0]-start[0])**2*tie_axis[0] - (target[1]-start[1])**2*tie_axis[1] - (target[2]-start[2])**2*tie_axis[2] <=0: - return pts,None - - curve_end = np.sqrt(Lcurve**2 - (target[0]-start[0])**2*tie_axis[0]-(target[1]-start[1])**2*tie_axis[1] - (target[2]-start[2])**2*tie_axis[2]) - - - i_target = [target[0], target[1], target[2]] - for j,axis in enumerate(tie_axis): - if axis == 0: - i_target[j] = start[j]+target[j]/abs(target[j])*(curve_end) - break - dxyz = np.array(i_target) - np.array(current) - dx = np.array(current) - np.array(start) - for i in np.arange(0.1,1.1,0.1): - x = i#/Lcurve#+1#/(10*(Lcurve)) + """ + Compute control points for a 3D curve from branch segment to tie point on wire. + + Uses vector projection to determine feasibility and compute the tie point location, + then generates a deflected curve using beam theory. + + Args: + target: Wire point (x, y, z) - a point on the wire + start: Branch segment start point (x, y, z) + current: Branch segment end point (x, y, z) + tie_axis: Unit direction vector of the wire (axis along which wire extends) + + Returns: + tuple: (control_points, tie_point) where: + - control_points: List of (x,y,z) tuples for curve fitting + - tie_point: Computed tie location on wire, or None if infeasible + + Geometry: + The branch, perpendicular offset to wire, and travel along wire form a right triangle: + - Hypotenuse = branch_length (||current - start||) + - One leg = perpendicular_distance (shortest distance from start to wire) + - Other leg = parallel_travel (distance to travel along wire to reach it) + """ + # Convert inputs to numpy arrays + start_arr = np.array([start[0], start[1], start[2]], dtype=float) + current_arr = np.array([current[0], current[1], current[2]], dtype=float) + wire_point = np.array([target[0], target[1], target[2]], dtype=float) + wire_axis = np.array(tie_axis, dtype=float) + + # Normalize wire axis to unit vector + wire_axis_norm = np.linalg.norm(wire_axis) + if wire_axis_norm < eps: + return [], None + wire_axis_unit = wire_axis / wire_axis_norm + + # Calculate branch segment length + segment_vector = current_arr - start_arr + branch_length = np.linalg.norm(segment_vector) + if branch_length < eps: + return [], None # Degenerate segment + + # Vector from branch start to wire point + v = wire_point - start_arr + + # Decompose v into components parallel and perpendicular to wire axis + parallel_component, perpendicular_component = self._get_parallel_and_perpendicular_components(v, wire_axis_unit) + perpendicular_distance = np.linalg.norm(perpendicular_component) + + # Feasibility check: branch must be long enough to reach the wire + if perpendicular_distance > branch_length: + return [], None + + # Calculate distance to travel along wire (Pythagorean theorem) + # branch_length² = perpendicular_distance² + parallel_travel² + parallel_travel_sq = branch_length**2 - perpendicular_distance**2 + parallel_travel = np.sqrt(max(0.0, parallel_travel_sq)) # Clamp to avoid floating-point negatives + + # Compute tie point on wire + # Start from perpendicular projection of start onto wire, then move parallel_travel along wire + start_projection_on_wire = start_arr + perpendicular_component + direction_to_wire = np.sign(np.dot(wire_point, wire_axis_unit)) + tie_point = start_projection_on_wire + parallel_travel * wire_axis_unit * direction_to_wire + + + # Generate control points along deflected curve using beam deflection formula + control_points = self._generate_deflected_curve(start_arr, current_arr, tie_point) + + return control_points, tuple(tie_point) + + def _generate_deflected_curve(self, start, current, tie_point): + control_points = [] + deflection_vector = np.array(tie_point) - np.array(current) + branch_length = np.linalg.norm(np.array(current) - np.array(start)) + for step in np.arange(0.1, 1.1, 0.1): + # Parametric position along branch segment [0.1, 0.2, ..., 1.0] + t = step - d = self.deflection_at_x(dxyz, x*Lcurve, Lcurve) - pts.append(tuple((start[0]+x*dx[0]+d[0],start[1]+x*dx[1]+d[1],start[2]+x*dx[2]+d[2]))) - return pts, i_target + # Base position: linear interpolation from start to current + base_position = start + t * (current - start) + + # Add beam deflection (cantilever formula) + deflection = self.deflection_at_x(deflection_vector, t * branch_length, branch_length) -# class Branch(BasicWood): -# def __init__(self, num_buds_segment: int = 5, bud_break_prob: float = 0.8, thickness: float = 0.1,\ -# thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ -# tie_axis: tuple = (0,1,1), bud_break_max_length: int = 5, order: int = 0, bud_break_prob_func: "Function" = lambda x,y: rd.random()): -# super().__init__(num_buds_segment, bud_break_prob, thickness, thickness_increment, growth_length,\ -# max_length, tie_axis, bud_break_max_length, order, bud_break_prob_func) - - -# class Trunk(BasicWood): -# """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ -# def __init__(self, num_buds_segment: int = 5, bud_break_prob: float = 0.8, thickness: float = 0.1,\ -# thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ -# tie_axis: tuple = (0,1,1), bud_break_max_length: int = 5, order: int = 0, bud_break_prob_func: "Function" = lambda x,y: rd.random()): -# super().__init__(num_buds_segment, bud_break_prob, thickness, thickness_increment, growth_length,\ -# max_length, tie_axis, bud_break_max_length, order, bud_break_prob_func) - -class Wire(): - """ Defines a trellis wire in the 3D space """ - def __init__(self, id:int, point: tuple, axis: tuple): - self.__id = id - self.__axis = axis - x,y,z = point - self.point = Vector3(x,y,z) - self.num_branch = 0 + # Combine base position and deflection + point = tuple(base_position + deflection) + control_points.append(point) + return control_points + + def _get_parallel_and_perpendicular_components(self, vec_a, vec_b): + # Project vec_a onto vec_b to get parallel and perpendicular components + vec_b_unit = vec_b / np.linalg.norm(vec_b) + parallel_component = np.dot(vec_a, vec_b_unit) * vec_b_unit + perpendicular_component = vec_a - parallel_component + return parallel_component, perpendicular_component - def add_branch(self): - self.num_branch+=1 + + + +class TreeBranch(BasicWood): + """Base class for all tree branch types with common initialization logic""" + + count = 0 # Class variable for instance counting + + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}, + name: str = None, contour_params: tuple = (1, 0.2, 30)): + + # Validate parameters + if copy_from is None and config is None: + raise ValueError("Either 'config' or 'copy_from' must be provided") + + # Call BasicWood constructor + super().__init__(config, copy_from) + + # Handle copy construction vs new instance + if copy_from: + # BasicWood already handled the copy, just set additional attributes + pass + else: + self.prototype_dict = prototype_dict + + # Set name with automatic numbering + if not name: + self.name = f"{self.__class__.__name__}_{self.__class__.count}" + self.__class__.count += 1 + + # Set up contour (subclasses can override contour_params) + radius, noise_factor, num_points = contour_params + self.contour = create_noisy_branch_contour(radius, noise_factor, num_points) + + # Initialize common attributes + self.num_buds = 0 + + # Initialize subclass-specific attributes + self._init_subclass_attributes() + + def _init_subclass_attributes(self): + """Hook for subclasses to initialize their specific attributes""" + pass + + def grow(self): + """Default empty implementation - subclasses can override if needed""" + pass + + +@dataclass +class Wire: + # All wires are horizontal, tying axis depends on wood definition + id: int + point: Tuple[float,float,float] + num_branch: int = 0 + + def add_branch(self): + self.num_branch += 1 + class Support(): """ All the details needed to figure out how the support is structured in the environment, it is a collection of wires""" - def __init__(self, points: list, num_wires: int, spacing_wires: int, trunk_wire_pt: tuple,\ - branch_axis: tuple, trunk_axis: tuple): + def __init__(self, points: list, num_wires: int, spacing_wires: int, trunk_wire_pt: tuple,): self.num_wires = num_wires self.spacing_wires = spacing_wires - self.branch_axis = branch_axis self.branch_supports = self.make_support(points)#Dictionary id:points - self.trunk_axis = None self.trunk_wire = None - if trunk_axis: - self.trunk_axis = trunk_axis - self.trunk_wire = Wire(-1, trunk_wire_pt, self.trunk_axis) #Make it a vector? + if trunk_wire_pt: + self.trunk_wire = Wire(-1, trunk_wire_pt) points.append(trunk_wire_pt) self.attractor_grid = Point3Grid((1,1,1),list(points)) @@ -231,6 +476,6 @@ def __init__(self, points: list, num_wires: int, spacing_wires: int, trunk_wire_ def make_support(self, points): supports = {} for id,pt in enumerate(points): - supports[id] = Wire(id, pt, self.branch_axis) + supports[id] = Wire(id, pt) return supports diff --git a/tree_generation/make_n_trees.py b/tree_generation/make_n_trees.py index c53fd4d..7e13f29 100644 --- a/tree_generation/make_n_trees.py +++ b/tree_generation/make_n_trees.py @@ -1,41 +1,105 @@ +import argparse +from dataclasses import dataclass import os +from pathlib import Path +import random import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) -import random as rd + from openalea.lpy import Lsystem + +from lpy_treesim import ColorManager from lpy_treesim.tree_generation.helpers import write -import argparse -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--num_trees', type=int, default=1) - parser.add_argument('--output_dir', type=str, default='dataset/') - parser.add_argument('--lpy_file', type=str, default='examples/UFO_tie_prune_label.lpy') - parser.add_argument('--verbose', action='store_true', default=False) + +BASE_LPY_PATH = Path(__file__).resolve().parents[1] / "base_lpy.lpy" + +# Ensure repository root is discoverable for prototype imports +sys.path.insert(0, str(BASE_LPY_PATH.parents[0])) + + +MAX_TREES = 99_999 + + +@dataclass +class TreeNamingConfig: + namespace: str + tree_type: str + + def _prefix(self, index: int) -> str: + if index > MAX_TREES: + raise ValueError(f"Tree index {index} exceeds maximum supported value {MAX_TREES}.") + return f"{self.namespace}_{self.tree_type}_{index:05d}" + + def mesh_filename(self, index: int) -> str: + return f"{self._prefix(index)}.ply" + + def color_map_filename(self, index: int) -> str: + return f"{self._prefix(index)}_colors.json" + + +def build_lsystem(tree_name: str) -> tuple[Lsystem, ColorManager]: + color_manager = ColorManager() + extern_vars = { + "prototype_dict_path": f"examples.{tree_name}.{tree_name}_prototypes.basicwood_prototypes", + "trunk_class_path": f"examples.{tree_name}.{tree_name}_prototypes.Trunk", + "simulation_config_class_path": f"examples.{tree_name}.{tree_name}_simulation.{tree_name}SimulationConfig", + "simulation_class_path": f"examples.{tree_name}.{tree_name}_simulation.{tree_name}Simulation", + "color_manager": color_manager, + "axiom_pitch": 0.0, + "axiom_yaw": 0.0, + } + lsystem = Lsystem(str(BASE_LPY_PATH), extern_vars) + return lsystem, color_manager + + +def generate_tree(lsystem: Lsystem, rng_seed: int, verbose: bool): + random.seed(rng_seed) + if verbose: + print(f"INFO: RNG seed {rng_seed}") + lstring = lsystem.axiom + for iteration in range(lsystem.derivationLength): + lstring = lsystem.derive(lstring, iteration, 1) + lsystem.plot(lstring) + return lstring, lsystem.sceneInterpretation(lstring) + + +def ensure_output_dir(path: Path): + path.mkdir(parents=True, exist_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description="Generate and save multiple L-Py trees.") + parser.add_argument('--num_trees', type=int, default=1, help='Number of trees to generate') + parser.add_argument('--output_dir', type=Path, default=Path('dataset/'), help='Directory for outputs') + parser.add_argument('--tree_name', type=str, default='UFO', help='Tree family to generate (UFO/Envy/etc.)') + parser.add_argument('--verbose', action='store_true', help='Print progress details') + parser.add_argument('--rng-seed', type=int, default=None, help='Optional deterministic seed') + parser.add_argument('--namespace', type=str, default='lpy', help='Prefix namespace for output filenames') args = parser.parse_args() - num_trees = args.num_trees - output_dir = args.output_dir - lpy_file = args.lpy_file - for i in range(num_trees): + if args.num_trees > (MAX_TREES + 1): + raise ValueError(f"num_trees={args.num_trees} exceeds supported maximum of {MAX_TREES + 1}.") + + naming = TreeNamingConfig(namespace=args.namespace, tree_type=args.tree_name) + ensure_output_dir(args.output_dir) + + rng = random.Random(args.rng_seed) + for index in range(args.num_trees): + lsystem, color_manager = build_lsystem(args.tree_name) + seed_value = rng.randint(0, 1_000_000) if args.verbose: - print("INFO: Generating tree number: ", i) - rand_seed = rd.randint(0,1000) - variables = {'label': True, 'seed_val': rand_seed} - l = Lsystem(lpy_file, variables) - lstring = l.axiom - for time in range(l.derivationLength): - lstring = l.derive(lstring, time, 1) - l.plot(lstring) - # l.plot() - scene = l.sceneInterpretation(lstring) - if not os.path.exists(output_dir): - os.makedirs(output_dir) + print(f"INFO: Generating {args.tree_name} tree #{index:03d}") + lstring, scene = generate_tree(lsystem, seed_value, args.verbose) + mesh_path = args.output_dir / naming.mesh_filename(index) + color_path = args.output_dir / naming.color_map_filename(index) + write(str(mesh_path), scene) + color_manager.export_mapping(str(color_path)) if args.verbose: - print("INFO: Writing tree number: ", i) - # scene.save("{}/tree_{}.obj".format(output_dir, i)) - write("{}/tree_{}.ply".format(output_dir, i), scene) + print(f"INFO: Wrote {mesh_path} and {color_path}") del scene del lstring - del l + del lsystem + + +if __name__ == "__main__": + main()