import random
import numpy as np
from binascii import hexlify as _hexlify
from os import urandom as _urandom
from openalea.mtg import *
from openalea.mtg import algo
from openalea.mtg import traversal
from openalea.hydroroot.law import length_law
#from random import choice
[docs]
def my_seed():
"""generate my own seed function to capture the seed value."""
return int(int(_hexlify(_urandom(2500)), 16) % 100000000)
[docs]
def linear(n=5):
"""Create a MTG with just one linear axis without properties.
:param n: (int) - number of vertices (Default value = 5)
:returns: g (MTG)
"""
g = MTG()
root = g.root
vid = g.add_component(root, order=0)
for i in range(n - 1):
vid = g.add_child(vid, edge_type='<', order=0)
return g
[docs]
def markov_binary_tree(g=None, vid=0, nb_vertices=1500,
branching_variability=0.1, branching_delay=20,
length_law=None,
nude_tip_length=200, order_max=5,
seed=None, censure_variability = False, **kwargs):
"""
Build a tree by modeling the stochastic process of lateral root branching by a first-order Markov chain on growing root axes.
The building process is constrained by the arguments, see parameter descriptions.
if `g` :code:`not None` then add the new tree to `g` at the vertex `vid`
:param g: (MTG) (Default value = None)
:param vid: (int) - the vertex id to which the new tree is added if not zero (Default value = 0)
:param nb_vertices: (int) - the number of vertices in the main axis (Default value = 1500)
:param branching_variability: (float) - variability on branching_delay and lateral lengths, :math:`\\in [0, 1]` (Default value = 0.1)
:param branching_delay: (float) - number of vertex between branching (Default value = 20)
:param length_law: function giving a length according to the branching position (Default value = None)
:param nude_tip_length: (float) - distance from tip without branching in vertices number (Default value = 200)
:param order_max: (int) - maximum root order (Default value = 5)
:param seed: (int) - a seed for the random generators (Default value = None)
:param censure_variability: (boolean) - deprecated because constrained by length_law- if True avoids to get a lateral longer than its parent (Default value = False)
:returns:
- g (MTG)
.. seealso:: :func:`hydroroot.law.length_law`, :func:`hydroroot.law.histo_relative_law`, :func:`hydroroot.main.hydroroot_mtg`
"""
# Modified FB 2020-03-10 : added flag in routine argument censure_variability see below
if g is None:
g = MTG()
if vid == 0:
vid = g.add_component(g.root, order=0)
anchors = [] # list of branching points where lateral roots will be "grafted"
if not seed is None:
random.seed(seed)
np.random.seed(seed)
several_laws=True if isinstance(length_law, list) else False
decrease = [ (1.-order/100.) for order in range(1, int(order_max)+1)]
def markov():
"""simple random markov chain - unused now"""
return 1 if random.random() < branching_variability else 0
def delayed_markov(timer):
"""markov chain with a delay between ramification
:param timer:
"""
if (timer <= 0) :
return (1,branching_delay)
else :
timer -= 1
return 0,timer
def random_delayed_markov(timer):
"""random markov chain with a delay between
possible ramification and uniform random variation
of branching position around mean position
:param timer:
"""
if (timer <= int(branching_variability*branching_delay)) :
return (1,branching_delay) if (random.random() < (1-branching_variability)) else (0,timer)
else :
timer -= 1
return 0,timer
def create_axis(nid, n, anchors=anchors):
"""create a random axis of length n and record the id of the branching points in anchors
:param nid: (int) - root node for the axis
:param n: (int) - number of vertices for this axis
:param anchors: (list) (Default value = anchors) - list of id of future ramification points on this axis
"""
axis = [markov() for i in range(n)]
for i in range(1,int(min(branching_delay,n))+1):
axis[-i] = 0
for ramif in axis:
order = nid.order
nid = nid.add_child(order=order, edge_type='<')
if ramif:
anchors.append(nid)
def create_delayed_axis(nid, n, anchors=anchors):
"""create an axis of length n using the delayed markov
and record the id of the branching points in anchors
:param nid: (int) - root node for the axis
:param n: (int) - number of vertices for this axis
:param anchors: (list) (Default value = anchors) - list of id of future ramification points on this axis
"""
axis = []
branch, time = delayed_markov(0)
for i in range(n-1):
branch, time = delayed_markov(time)
if (n-i) > (nude_tip_length): # check axis length compared to minimal branching length
axis.append((branch, n-i))
else : # leave end of axis empty of branching
axis.append((0,0))
for ramif, position in axis:
order = nid.order
nid = nid.add_child(order=order, edge_type='<')
nid.position_index = position
if ramif:
anchors.append(nid)
def create_randomized_delayed_axis(nid, n, anchors=anchors):
"""create an axis of length n using the delayed markov
and randomized the id of the branching points in anchors
around the theoretical branching positions
:param nid: (int) - root node for the axis
:param n: (int) - number of vertices for this axis
:param anchors: (list) (Default value = anchors) - list of id of future ramification points on this axis
"""
n = int(n)
axis = []
shuffled_axis = []
branch, time = delayed_markov(0)
for i in range(n-1):
branch, time = delayed_markov(time)
axis.append((branch, n-i))
shuffled_axis.append((branch,n-i))
for i in range(n-1):
# shift (1-branching_variability) branching points
# at max (1-branching_variability)*branching delay away from
# theoretical branching position
if (axis[i][0] == 1): # read 'axis' only to avoid treating the same branching point after each shift
if 1 : #random.random() < branching_variability :
var = int(round(branching_variability*branching_delay))
shift = random.randint(-var,var)
# shift occurs only if the target is not branched already or outside the axis
if ((i+shift)>0) and ((i+shift)<n-1) and (shuffled_axis[i+shift][0]==0) :
b, p = shuffled_axis[i]
shuffled_axis[i] = (0, p)
shuffled_axis[i+shift] = (1, p)
for ramif, position in shuffled_axis:
order = nid.order
nid = nid.add_child(order=order, edge_type='<')
nid.position_index = position
if ramif :
anchors.append(nid)
# create_axis(g.node(vid), nb_vertices) # deprecated
#print 'entering MTG building'
# Create the first axis
create_randomized_delayed_axis(g.node(vid), nb_vertices)
while anchors: # while they are branching point left
nid = anchors.pop(0) # take next branching point
position_index = nid.position_index # distance to the tip
#print position_index
current_order = nid.order
if nid.order < order_max: # check if maximal branching order was reached
# if there is a length law, use it to compute lateral root length at this position
if length_law:
if several_laws:
current_law = length_law[0] if current_order == 0 else length_law[1]
lateral_length = int(current_law(position_index))
else:
lateral_length = int(length_law(position_index))
else : # standard lateral root length - can't be longer than the bearing axis remaining branching length (remaining length - nude tip length)
n = len(list(algo.descendants(g,nid._vid,RestrictedTo='SameAxis')))
#n = random.randint(1, max(n-nude_tip_length,1))
n = max(n-nude_tip_length,1)
lateral_length = n-1
real_lateral_length = lateral_length
# create axis of appropriate length
if lateral_length > 0:
# branching_variability also apply to the length of LR
var = int(lateral_length*branching_variability)
lateral_length = random.randint(max(1,lateral_length-var), lateral_length+var)
# Censure variability
# Modified FB 2020-03-10 : by default lateral lengths not constrained to nb_vertices*decrease[current_order]
# so it is an "arbitrary" constrain on length
# the lateral lengths are constrained to the length law in def histo_relative_law
if censure_variability:
if lateral_length > nb_vertices*decrease[current_order]:
if real_lateral_length <= nb_vertices*decrease[current_order]:
lateral_length = real_lateral_length
else:
print(("WARNING: lateral length is too large ", lateral_length))
lateral_length = nb_vertices*decrease[current_order]
# Create the first node of the branching point and the corresponding axis
cid = nid.add_child(order=nid.order+1, edge_type='+')
#print "pid length", nid, lateral_length
#print "Var, Lateral length: ", var, lateral_length
create_randomized_delayed_axis(cid, lateral_length)
fat_mtg(g)
#print 'exiting MTG building'
return g
[docs]
def shuffle_axis(g=None, shuffle=False):
"""For each subtree of a MTG, change its root node to another node of the same axis.
:param g: (Default value = None)
:param shuffle: (Default value = False)
:returns: - g
"""
max_scale = g.max_scale()
if shuffle:
#print 'entering axis shuffling'
v_base = next(g.component_roots_at_scale_iter(g.root, scale=max_scale))
shuffling = {}
shuffled = []
ramifs = (v for v in g.vertices_iter(scale=max_scale) if g.edge_type(v) == '+')
for v in ramifs:
axis = g.Axis(g.parent(v)) # list of all node in the same axis as v
shuffling[v] = random.choice(axis) # record new position for each subtree
for v in traversal.post_order2(g,v_base):
if v in list(shuffling.keys()) and v not in shuffled:
sub = g.sub_tree(v, copy=True) # get subtree
g.remove_tree(v) # prune it from old position
newbranch = g.add_child_tree(shuffling[v], sub) # insert subtree at previously determined position
shuffled.append(newbranch[0]) # keep track of shifted tree id
#print 'exiting axis shuffling'
return g
[docs]
def generate_g(seed = None, length_data = None, branching_variability = 0.25,
delta = 2e-3, nude_length = 2e-3, primary_length = 0.13, segment_length = 1e-4, order_max = 4):
"""generate a MTG according to the input parameters using length_data (mendatory) for the branching
Preprocessing variables before calling :func:`markov_binary_tree`:
- nb_vertices: primary root length in vertex number
- branching_delay: distance between branching in vertex number
- nb_nude_vertices: distance from tip without branching in vertex number
- length_law: the function giving the lateral length according to its position on the parent branch
:param seed: (int) - the seed for the random generator in the markof chain (Default value = None)
:param length_data: (Dataframe) - pandas dataframe columns names 'LR_length_mm' and 'relative_distance_to_tip' sorted by 'relative_distance_to_tip' (Default value = None)
:param branching_variability: (float) - variability on branching_delay and lateral lengths, :math:`\\in [0, 1]` (Default value = 0.25)
:param delta: (float) - reference distance (m) between successive branching axis (Default value = 2e-3)
:param nude_length: (float) - length (m) at root tip with no ramification (Default value = 2e-3)
:param primary_length: (float) - primary root length (m) (Default value = 0.13)
:param segment_length: (float) - length of the vertices (m) (Default value = 1e-4)
:param order_max: (int) - maximum lateral roots order (Default value = 4)
:returns:
- g: MTG with the following properties set: edge_type, label, position
.. seealso:: :func:`hydroroot.law.length_law`, :func:`hydroroot.law.histo_relative_law`, :func:`hydroroot.main.root_builder`
"""
# nude length and branching delay in terms of number of vertices
nb_nude_vertices = int(nude_length / segment_length)
branching_delay = int(delta / segment_length)
nb_vertices = int(primary_length / segment_length)
# F. Bauget 2022-08-12: added if-else to be able to use the function without length data, useful for usage demo
if length_data:
length_max_secondary = length_data[0].LR_length_mm.max() * 1e-3 # in m
law_order1 = length_law(length_data[0], scale_x = primary_length / 100., scale = segment_length)
law_order2 = length_law(length_data[1], scale_x = length_max_secondary / 100., scale = segment_length)
_length_law = [law_order1, law_order2]
else:
_length_law = None
g = markov_binary_tree(
nb_vertices = nb_vertices,
branching_variability = branching_variability,
branching_delay = branching_delay,
length_law = _length_law,
nude_tip_length = nb_nude_vertices,
order_max = order_max,
seed = seed)
return g