def __init__(self, birthdate=None, id=None, min_radius=0.001, latency=6, store_data=True): """**Constructor** :param datetime.datetime birthdate: :param int id: :param float min_radius: in meters :param int store_data: :param float latency: number of days of latency before stopping the gu. :attributes: * :attr:`length`: total length of the growth unit in meters * :attr:`radius`: radius of at the base of the growth unit (see plants module) * those inherited by :class:`~openalea.plantik.biotik.component.ComponentInterface`: * :attr:`age` * :attr:`demand` * :attr:`birthdate` * :attr:`state` set to 'growing' by default in the constructor. Then, the :meth:`update` may set to it to 'stopped' if no changement has been done for a duration> :attr:`latency`. * :attr:`internode_counter`: number of internodes in the growth unit * :attr:`variables` is a :class:`CollectionVariables` instance containing the :attr:`age`, :attr:`radius` and :attr:`length` at each time step .. note:: when creating a gu, :attr:`state` is by definition set to 'growing'. """ self.context = Context() # when creating a gu, it is by definition in a growing state. ComponentInterface.__init__(self, label='GrowthUnit', birthdate=birthdate, id=id, state='growing') self.store_data = store_data self._length = 0. self._radius = min_radius self._latency = latency self._internode_counter = 0. self.variables = CollectionVariables() self.variables.add(SingleVariable(name='age', unit='days', values=[self.age.days])) self.variables.add(SingleVariable(name='length', unit='meters', values=[self.length])) self.variables.add(SingleVariable(name='radius', unit='meters', values=[self.radius])) self.__step_without_growing = 0
def __init__(self, time_step, options=None, revision=None, pipe_fraction=1., filename=None, tag=None): """**Constructor** :param float time_step: the time step of the simulation. :param options: a variable containing the simulation options from the config file (see :class:`~vplants.plantik.tools.config.ConfigParams`) :param str revision: a SVN revision for book keeping. :param float pipe_fraction: :param str filename: if None, populates attribute :attr:`filename` with 'plant' :param str tag: append to the filename if not None :param pipe_fraction: cost of the pipe model is metamer_growth volume times the pipe_fraction parameter >>> from vplants.plantik import get_shared_data >>> from vplants.plantik.tools import ConfigParams >>> from vplants.plantik.biotik import Plant >>> options = ConfigParams(get_shared_data('pruning.ini')) >>> plant = Plant(1, options, revision=1, tag='test') >>> plant.filename 'plant_test' >>> plant.dt 1 :Attributes: * :attr:`revision` : * :attr:`options` : * :attr:`time` : * :attr:`filename`: * :attr:`mtg`: * :attr:`lstring`: used to store the lstring of an lsystem * :attr:`dt`: * :attr:`mtgtools`: an attribute of type :class:`~vplants.plantik.tools.mtgtools.MTGTools` that is used to store the mtg and retrieve many relevant information. It also has a DB facility. * :attr:`variables`: a :class:`~vplants.plantik.biotik.collection.CollectionVariables` instance containing the `pipe_ratio` variable. pipe_ratio contains is the ratio of resource attributed to the pipe model over time. * :attr:`counter`: a :class:`~vplants.plantik.biotik.collection.CollectionVariables` instance containing the count of leaves/internodes/apices/growth units/branches over time. See also :meth:`plot_counter` and :meth:`update_counter`. * :attr:`DARC`: a :class:`~vplants.plantik.biotik.collection.CollectionVariables` instance containing the DARC values. See also :meth:`plot_DARC` and :meth:`update_DARC`. DARC stands for Demand, Allocated, Resource, Cost .. todo:: clean up all other variables that could be extracted from the lstring ? .. todo:: difference pipe_ratio pipe_fraction .. todo:: duration, apex, all, dv """ # RESERVE related try: self.reserve_duration = options.reserve.duration # used for the reserve sigmoid self.reserve_starting_time = options.reserve.starting_time # used for the reserve sigmoid assert options.reserve.alpha >= 0 and options.reserve.alpha <= 1, 'options reserve.alpha must be in [0,1] in ini file.' self.reserve_alpha = options.reserve.alpha self.verbose = options.general.verbose except: self.reserve_duration = 210 self.reserve_starting_time = 105 self.reserve_alpha = 1 self.verbose = True self.Reserve = 0 #FILENAME if filename != None: assert type(filename) == str if tag != None: assert type(tag) == str self._filename = self._set_filename(filename, tag) self._revision = revision self._options = options self._time = [] self.age = 0 self._mtg = None self._lstring = None self.label = 'Plant' # could be retrieved from options parameter but let us try to not depends on options ! self._dt = time_step #!! when calling self.mtg, what you really do is self.mtgtools.mtg # !!when doing self.mtg = a, what you really do is self.mtgtools.mtg = a self.mtgtools = MTGTools(verbose=self.verbose) self.D = 0 try: self.R = options.root.initial_resource except: self.R = 1 self.C = 0 self.A = 0 self.allocated = [] self.duration = 0 # used to store the duration of the simulation self.apex = { 'demand': [], 'allocated': [], 'height': [], 'age': [] } # keeps track of the main apex characteristics self.all = {'age': [], 'order': [], 'height': []} #: expected increase of volume related to the pipe model. self.dV = 0 #PIPE FRACTION assert pipe_fraction <= 1 and pipe_fraction >= 0 self.pipe_fraction = pipe_fraction self.radius = 0 #: extract storeage for variables over time inluding :attr:`pipe_ratio` and :attr:`dV`. self.variables = CollectionVariables() self.variables.add( SingleVariable(name='pipe_ratio', unit='ratio', values=[])) self.variables.add(SingleVariable(name='dV', unit='ratio')) self.variables.add(SingleVariable(name='reserve', unit='UR')) self.year = 1 self._reserve_function = GrowthFunction( 0, 1, maturation=(self.year) * self.reserve_duration - self.reserve_starting_time - (self.year - 1) * 365, growth_rate=0.1, growth_function='sigmoid', nu=1) #: a CollectionVariable instance that containes the count of # **apices**, **internodes**, **leaves** #: **branches** and growth units (denoted **gus**) at each time step. #For instance, to acces to apices counter:: #: #: plant.counter.apices.values self.counter = CollectionVariables() self.counter.add(SingleVariable(name='apices', unit=r'#')) self.counter.add(SingleVariable(name='internodes', unit=r'#')) self.counter.add(SingleVariable(name='leaves', unit=r'#')) self.counter.add(SingleVariable(name='branches', unit=r'#')) self.counter.add(SingleVariable(name='gus', unit=r'#')) #: a CollectionVariable instance that contains the demand (D), allocated resources(R), #: resources (R) and cost (C) at each time step. To acces to demand over time:: #: #: >>> plant.DARC.D.values self.DARC = CollectionVariables() self.DARC.add(SingleVariable(name='D', unit='biomass unit', values=[])) self.DARC.add(SingleVariable(name='A', unit='biomass unit', values=[])) self.DARC.add(SingleVariable(name='R', unit='biomass unit', values=[])) self.DARC.add(SingleVariable(name='C', unit='biomass unit', values=[])) self.DARC.add(SingleVariable(name='pipe_cost', unit='biomass unit'))
class GrowthUnit(ComponentInterface): """Specialised version of :class:`~openalea.plantik.biotik.component.ComponentInterface` dedicated to GrowthUnits. GrowthUnit class does not compute anything special, it mainly serves as storage for various information. The update of the length and radius is made in the :mod:`~openalea.plantik.biotik.plant` module with the method :meth:`~openalea.plantik.biotik.plant.Plant.growth_unit_update` :Example: >>> from vplants.plantik.biotik.growthunit import * >>> gu = GrowthUnit() >>> gu.radius 0.001 >>> gu.variables.radius.values [0.001] :plotting: If the store_data is True, the you can plot results either from the class instance :meth:`plot` of from the variables stored in :attr:`variables`. The former being less flexible with only `plot` of radius versus age, length versus age, length versus radius. And the latter provides plot of variable versus age (the same as before) as well as histograms. :Notation: * age of a growth unit is denoted :math:`t_a^{(gu)}` .. csv-table:: **Notation related to** :class:`~openalea.plantik.biotik.growthunit.GrowthUnit` :header: Name, symbol, default value, type :widths: 15,20,20, 20 radius_min , :math:`r^{(gu)}_{min}` , 0.001 (same as internode min radius), user parameter radius , :math:`r^{(gu)}` , - , attribute length , :math:`l^{(gu)}` , length of the growth unit ,attribute # internode , :math:`N_{(i)}^{(gu)}` , 0 by default ,attribute latency , :math:`T^{(gu)}_{latency}`, default is 6 days , user parameter """ def __init__(self, birthdate=None, id=None, min_radius=0.001, latency=6, store_data=True): """**Constructor** :param datetime.datetime birthdate: :param int id: :param float min_radius: in meters :param int store_data: :param float latency: number of days of latency before stopping the gu. :attributes: * :attr:`length`: total length of the growth unit in meters * :attr:`radius`: radius of at the base of the growth unit (see plants module) * those inherited by :class:`~openalea.plantik.biotik.component.ComponentInterface`: * :attr:`age` * :attr:`demand` * :attr:`birthdate` * :attr:`state` set to 'growing' by default in the constructor. Then, the :meth:`update` may set to it to 'stopped' if no changement has been done for a duration> :attr:`latency`. * :attr:`internode_counter`: number of internodes in the growth unit * :attr:`variables` is a :class:`CollectionVariables` instance containing the :attr:`age`, :attr:`radius` and :attr:`length` at each time step .. note:: when creating a gu, :attr:`state` is by definition set to 'growing'. """ self.context = Context() # when creating a gu, it is by definition in a growing state. ComponentInterface.__init__(self, label='GrowthUnit', birthdate=birthdate, id=id, state='growing') self.store_data = store_data self._length = 0. self._radius = min_radius self._latency = latency self._internode_counter = 0. self.variables = CollectionVariables() self.variables.add(SingleVariable(name='age', unit='days', values=[self.age.days])) self.variables.add(SingleVariable(name='length', unit='meters', values=[self.length])) self.variables.add(SingleVariable(name='radius', unit='meters', values=[self.radius])) self.__step_without_growing = 0 def _getInternode(self): return self._internode_counter def _setInternode(self, value): prev = self._internode_counter self._internode_counter = value if self._internode_counter == prev: self.__step_without_growing += 1 else: self.__step_without_growing = 0 internode_counter = property(_getInternode, _setInternode, None, "getter/setter to the GU radius") def _getRadius(self): return self._radius def _setRadius(self, value): if value< self._radius: raise ValueError("radius decreased in GU update!!") self._radius = value radius = property(_getRadius, _setRadius, None, doc="getter/setter to the growth unit radius. Cannot decrease !") def _getLength(self): return self._length def _setLength(self, value): self._length = value length = property(_getLength, _setLength, None, "getter/setter to the GU length") def _getLatency(self): return self._latency latency = property(_getLatency, None, None, "getter to the GU latency") def update(self, dt): """Update the GU characteristics at each time step This function updates the :attr:`age` of the component by **dt** Moreover, if **store_data** is True, the age, length and radius :attr:`variables` are also stored at each time step as long as the growth unit state is *growing*, which is True when the attribute :attr:`__step_without_growing` times :math:`\Delta t` is greater than the latency :math:`T_{latency}^{(gu)}` Tasks: #. increment age by dt #. calls :meth:`demandCalculation`, :meth:`resourceCalculation` and :meth:`computeLivingCost` #. store age into variables.age #. store radius into variables.radius #. store length into variables.length :param float dt: in days """ super(GrowthUnit, self).update(dt) self.demandCalculation() self.resourceCalculation() if self.store_data is True and self.state == 'growing': self.variables.age.append(self.age.days) self.variables.length.append(self.length) self.variables.radius.append(self.radius) if self.__step_without_growing * dt > self.latency: self.state = 'stopped' def demandCalculation(self, **kargs): """no demand for a gu (i.e., zero)""" pass def resourceCalculation(self, **kargs): """no resource for a gu (i.e., zero)""" pass def plot(self, variables=['radius', 'length'], show=True, grid=True, **args): """plot some results :param list variables: plot results related to the variables provided :param bool show: create but do not show the plot (useful for test, saving) :param args: any parameters that pylab.plot would accept. .. plot:: :width: 30% :include-source: from vplants.plantik.biotik.growthunit import * b = GrowthUnit() for v in range(1,100): b.radius = (v*0.001)**0.5 b.length = v*0.01 if v>50: b.state = 'stopped' b.update(1) b.plot('radius') """ self.variables.plot(variables=variables, show=show, grid=grid, **args) def __str__(self): from vplants.plantik.tools.misc import title res = super(GrowthUnit, self).__str__() res += self.context.__str__() res += self.variables.__str__() res += title('other growth unit attributes') res += '\n' for name in self.variables.keys(): res += "%s = %s" % (name, getattr(self, name)) res += '\n' return res
def __init__(self, resource_per_day, birthdate=None, id=None, maturation=21, internode_vigor=1., livingcost=0., growth_rate=1, area_optimal=30 * 0.01 * 0.01, growth_function='sigmoid', efficiency_method='unity', store_data=False, nu=1, t1=15, t2=150, angle=0.): """**Constructor** :param float resource_per_day: stricly positive , notation :math:`r_0`. :param datetime.datetime birthdate: :param int id: :param float maturation: leaf maturation in days :param float internode_vigor: a leaf area is proportional to the internode length. intenode_vigor of 1 means the internode had full length and therefore leaf has a potential for maximum area as well. :param float livingcost: zero by default :param float growth_rate: the :math:`lambda` parameter of the growth function :param str growth_function: a GrowthFunction method in ['linear', 'sigmoid', 'logistic'] :param nu: shape of the logistic function is :attr:`growth_function` is logisitic :param efficiency_method: 'unity' or 'sigmoid' (default is unity) :param int store_data: :param float t1: parameter of the leaf_efficiency method :param float t2: parameter of the leaf_efficiency method :attributes: * :attr:`area`: the leaf area, a :meth:`~vplants.plantik.biotik.growth.GrowthFunction` instance (read-only). The input parameters **growth_function**, **growth_rate**, **nu**, and **maturation** are used by this function. * Those inherited by :class:`~vplants.plantik.biotik.component.ComponentInterface`: :attr:`age`, :attr:`allocated`, :attr:`demand`, :attr:`livingcost`, :attr:`~vplants.plantik.biotik.component.ComponentInterface.resource`. * :attr:`variables` is a :class:`CollectionVariables` instance containing the :attr:`age`, :attr:`demand`, :attr:`resource`, :attr:`area`. """ assert internode_vigor >= 0 and internode_vigor <= 1, \ "internode vifor must be in [0,1]" assert resource_per_day > 0, \ 'resource_per_day must be positive otherwise noting will happen...' self.context = Context() ComponentInterface.__init__(self, label='Leaf', birthdate=birthdate, id=id, state='growing') # leaf min must correspond to internode length min so that # leafmaxarea>-leafminarea self._radius = Leaf.petiole_radius # for geometry purpose only # set the area growth function self.area_max = Leaf.area_max * internode_vigor self._area = GrowthFunction(Leaf.area_min, self.area_max, maturation=maturation, growth_rate=growth_rate, growth_function=growth_function, nu=nu) self._r0 = resource_per_day self.angle = angle # used for bookeeping only self.internode_vigor = internode_vigor # in % self.maturation = maturation # in days # used by update() self.store_data = store_data # some variables to store. self.variables = CollectionVariables() self.variables.add( SingleVariable(name='age', unit='days', values=[self.age.days])) self.variables.add( SingleVariable(name='resource', unit='biomass unit', values=[self.resource])) self.variables.add( SingleVariable(name='demand', unit='biomass unit', values=[self.demand])) self.variables.add( SingleVariable(name='area', unit='square meters', values=[self.area])) # parameters self.livingcost = livingcost # cost to maintain leaf alive !! self.efficiency_method = efficiency_method # other attributes. # radius used by the interpretation of the lsystem. #todo: move it elsewhere outsitde this class. self.father_radius = 0 #others self.lg = 0. # light interception # related to leaf efficiency self._leaf_efficiency = 1. self.t1 = t1 self.t2 = t2 self.growth_rate = growth_rate
class Plant(object): """A Plant Factory to store and compute various information within a simulation This class should be used as the first module of an axialtree/lstring/mtg. It is used by the reactive model to #. store various information such as the simulation options and revision. #. compute the **DARC** numbers at each time step #. compute the pipe model cost at each time step #. store MTG/lstring #. provide facilities to extract informtion from lstring/mtg """ def __init__(self, time_step, options=None, revision=None, pipe_fraction=1., filename=None, tag=None): """**Constructor** :param float time_step: the time step of the simulation. :param options: a variable containing the simulation options from the config file (see :class:`~vplants.plantik.tools.config.ConfigParams`) :param str revision: a SVN revision for book keeping. :param float pipe_fraction: :param str filename: if None, populates attribute :attr:`filename` with 'plant' :param str tag: append to the filename if not None :param pipe_fraction: cost of the pipe model is metamer_growth volume times the pipe_fraction parameter >>> from vplants.plantik import get_shared_data >>> from vplants.plantik.tools import ConfigParams >>> from vplants.plantik.biotik import Plant >>> options = ConfigParams(get_shared_data('pruning.ini')) >>> plant = Plant(1, options, revision=1, tag='test') >>> plant.filename 'plant_test' >>> plant.dt 1 :Attributes: * :attr:`revision` : * :attr:`options` : * :attr:`time` : * :attr:`filename`: * :attr:`mtg`: * :attr:`lstring`: used to store the lstring of an lsystem * :attr:`dt`: * :attr:`mtgtools`: an attribute of type :class:`~vplants.plantik.tools.mtgtools.MTGTools` that is used to store the mtg and retrieve many relevant information. It also has a DB facility. * :attr:`variables`: a :class:`~vplants.plantik.biotik.collection.CollectionVariables` instance containing the `pipe_ratio` variable. pipe_ratio contains is the ratio of resource attributed to the pipe model over time. * :attr:`counter`: a :class:`~vplants.plantik.biotik.collection.CollectionVariables` instance containing the count of leaves/internodes/apices/growth units/branches over time. See also :meth:`plot_counter` and :meth:`update_counter`. * :attr:`DARC`: a :class:`~vplants.plantik.biotik.collection.CollectionVariables` instance containing the DARC values. See also :meth:`plot_DARC` and :meth:`update_DARC`. DARC stands for Demand, Allocated, Resource, Cost .. todo:: clean up all other variables that could be extracted from the lstring ? .. todo:: difference pipe_ratio pipe_fraction .. todo:: duration, apex, all, dv """ # RESERVE related try: self.reserve_duration = options.reserve.duration # used for the reserve sigmoid self.reserve_starting_time = options.reserve.starting_time # used for the reserve sigmoid assert options.reserve.alpha >= 0 and options.reserve.alpha <= 1, 'options reserve.alpha must be in [0,1] in ini file.' self.reserve_alpha = options.reserve.alpha self.verbose = options.general.verbose except: self.reserve_duration = 210 self.reserve_starting_time = 105 self.reserve_alpha = 1 self.verbose = True self.Reserve = 0 #FILENAME if filename != None: assert type(filename) == str if tag != None: assert type(tag) == str self._filename = self._set_filename(filename, tag) self._revision = revision self._options = options self._time = [] self.age = 0 self._mtg = None self._lstring = None self.label = 'Plant' # could be retrieved from options parameter but let us try to not depends on options ! self._dt = time_step #!! when calling self.mtg, what you really do is self.mtgtools.mtg # !!when doing self.mtg = a, what you really do is self.mtgtools.mtg = a self.mtgtools = MTGTools(verbose=self.verbose) self.D = 0 try: self.R = options.root.initial_resource except: self.R = 1 self.C = 0 self.A = 0 self.allocated = [] self.duration = 0 # used to store the duration of the simulation self.apex = { 'demand': [], 'allocated': [], 'height': [], 'age': [] } # keeps track of the main apex characteristics self.all = {'age': [], 'order': [], 'height': []} #: expected increase of volume related to the pipe model. self.dV = 0 #PIPE FRACTION assert pipe_fraction <= 1 and pipe_fraction >= 0 self.pipe_fraction = pipe_fraction self.radius = 0 #: extract storeage for variables over time inluding :attr:`pipe_ratio` and :attr:`dV`. self.variables = CollectionVariables() self.variables.add( SingleVariable(name='pipe_ratio', unit='ratio', values=[])) self.variables.add(SingleVariable(name='dV', unit='ratio')) self.variables.add(SingleVariable(name='reserve', unit='UR')) self.year = 1 self._reserve_function = GrowthFunction( 0, 1, maturation=(self.year) * self.reserve_duration - self.reserve_starting_time - (self.year - 1) * 365, growth_rate=0.1, growth_function='sigmoid', nu=1) #: a CollectionVariable instance that containes the count of # **apices**, **internodes**, **leaves** #: **branches** and growth units (denoted **gus**) at each time step. #For instance, to acces to apices counter:: #: #: plant.counter.apices.values self.counter = CollectionVariables() self.counter.add(SingleVariable(name='apices', unit=r'#')) self.counter.add(SingleVariable(name='internodes', unit=r'#')) self.counter.add(SingleVariable(name='leaves', unit=r'#')) self.counter.add(SingleVariable(name='branches', unit=r'#')) self.counter.add(SingleVariable(name='gus', unit=r'#')) #: a CollectionVariable instance that contains the demand (D), allocated resources(R), #: resources (R) and cost (C) at each time step. To acces to demand over time:: #: #: >>> plant.DARC.D.values self.DARC = CollectionVariables() self.DARC.add(SingleVariable(name='D', unit='biomass unit', values=[])) self.DARC.add(SingleVariable(name='A', unit='biomass unit', values=[])) self.DARC.add(SingleVariable(name='R', unit='biomass unit', values=[])) self.DARC.add(SingleVariable(name='C', unit='biomass unit', values=[])) self.DARC.add(SingleVariable(name='pipe_cost', unit='biomass unit')) def __str__(self): res = "R:" + str(self.R) + "\n" res += "D:" + str(self.D) + "\n" res += "C:" + str(self.C) + "\n" return res def plot_PARC(self, show=True, normalised=True, savefig=False, num_fig=1, width=1, linewidth=1): """plotting dedicated to the :attr:`PARC` attribute. where P stand for pipe cost :param bool show: :param bool savefig: :param bool normalised: normalise the quantity D, A, R, C by R (total resource) .. todo: this doc to be cleaned up .. plot:: :include-source: :width: 50% from vplants.plantik import * options = ConfigParams(get_shared_data('pruning.ini')) plant = Plant(1, options) for x in range(100): plant.DARC.D.append(0.25) plant.DARC.A.append(0.25) plant.DARC.R.append(1) plant.DARC.C.append(0.25) plant.DARC.pipe_cost.append(0.5) plant._time.append(x) plant.plot_PARC() """ from pylab import bar, hold, legend, title, figure, clf, xlabel, plot, ylabel import numpy T = numpy.array(self.time) D = numpy.array(self.DARC.D.values) A = numpy.array(self.DARC.A.values) Rn = numpy.array(self.DARC.R.values) C = numpy.array(self.DARC.C.values) Reserve = numpy.array(self.variables.reserve.values) if normalised == False: R = Rn / Rn else: R = Rn pipe = numpy.array(self.DARC.pipe_cost.values) figure(num_fig) clf() bar(T, A / R, label='Primary growth, A', width=width, linewidth=linewidth) hold(True) bar(T, C / R, bottom=A / R, label='Living cost, C', color='r', width=width, linewidth=linewidth) bar(T, (pipe / R), bottom=(C + A) / R, color='g', label='Secondary growth', width=width, linewidth=linewidth) bar(T, (Reserve / R), bottom=(C + A + pipe) / R, color='y', label='Reserve', width=width, linewidth=linewidth) plot(T, Rn, color='k', label='Resource, R', linewidth=2) plot(T, D, color='k', label='Demand, D', linewidth=1, linestyle='--') legend(loc='best') xlabel('Time (days)') ylabel('Unit Resource') title("Proportion of allocation, pipe cost and living cost") if show is True: from pylab import show as myshow myshow() def plot_DARC(self, show=True, normalised=False, savefig=False, num_fig=1): """plotting dedicated to the :attr:`DARC` attribute. :param bool show: :param bool savefig: :param bool normalised: normalise the quantity D, A, R, C by R (total resource) .. plot:: :include-source: :width: 50% from vplants.plantik.biotik.plant import * from vplants.plantik import get_shared_data, ConfigParams options = ConfigParams(get_shared_data('pruning.ini')) plant = Plant(1,options) for x in range(100): plant.DARC.D.append(x**0.5) plant.DARC.A.append(x**0.3) plant.DARC.R.append(x**0.4) plant.DARC.C.append(x**0.3) plant.DARC.pipe_cost.append(x**0.3) plant._time.append(x) plant.plot_DARC() """ import pylab import numpy pylab.rcParams['text.usetex'] = True fig = pylab.figure(num_fig) pylab.clf() if normalised == True: norm = numpy.array(self.R) else: norm = 1. T = numpy.array(self.time) D = numpy.array(self.DARC.D.values) A = numpy.array(self.DARC.A.values) R = numpy.array(self.DARC.R.values) C = numpy.array(self.DARC.C.values) try: pipe = numpy.array(self.DARC.pipe_cost.values) except: pass pylab.plot(T, D / norm, '-ob', label="Total demand") pylab.hold(True) pylab.plot(T, A / norm, '-.sr', label="Allocation cost") pylab.plot(T, R / norm, '-k', linewidth=2, label='Total resource') pylab.plot(T, C / norm, '-D', color='magenta', markerfacecolor=None, markersize=12, label='Total cost') pylab.plot(T, pipe / norm, '-og', label='Pipe model cost') try: pylab.plot(T, (C + A + pipe) / norm, '-square', markersize=15, label='Total cost + allocation + pipe\_model cost') except: pass pylab.xlabel('Time (days)') pylab.ylabel(r'\#') pylab.legend(loc='best') pylab.grid(True) if show: pylab.show() if savefig: pylab.savefig(self.filename + "_DARC.png") def plot_counter(self, show=True, savefig=False, num_fig=1): """ plotting dedicated to counter :param bool show: :param bool savefig: .. plot:: :include-source: :width: 50% from vplants.plantik.biotik.plant import * from vplants.plantik import get_shared_data, ConfigParams options = ConfigParams(get_shared_data('pruning.ini')) plant = Plant(1,options) for x in range(100): plant.counter.apices.append(x**0.5) plant.counter.leaves.append(x**0.35) plant.counter.internodes.append(x**0.4) plant.counter.branches.append(x**0.3) plant._time.append(x) plant.plot_counter() """ import pylab pylab.rcParams['text.usetex'] = True fig = pylab.figure(num_fig) pylab.clf() pylab.semilogy(self.time, self.counter.apices.values, label='Apex number') pylab.hold(True) try: pylab.semilogy(self.time, self.counter.internodes.values, label='Internode number') except: pass try: pylab.semilogy(self.time, self.counter.leaves.values, label='Leaves number') except: pass try: pylab.semilogy(self.time, self.counter.branches.values, label='branches') except: pass try: pylab.semilogy(self.time, self.counter.gus.values, label='gu') except: pass pylab.xlabel('Time (days)') pylab.ylabel(r'\#') pylab.legend(loc='best') pylab.grid(True) if show: pylab.show() if savefig: pylab.savefig(self.filename + "_counter.png") def plot(self, normalised=False, show=True, savefig=False): """calls all plotting methods calls :meth:`plot_counter()`, :meth:`plot_DARC()` and :meth:`plot_PARC()`. many more plots can be found in :attr:`mtgtools` attribute """ self.plot_counter(num_fig=1, savefig=savefig, show=show) self.plot_DARC(normalised=normalised, show=show, num_fig=2, savefig=savefig) self.plot_PARC(normalised=normalised, show=show, num_fig=3, savefig=savefig) def update_counter(self, lstring): """Parse the lstring attribute and count the number of elements to update the :attr:`counter` attribute. .. warning:: you should use the :meth:`update` to call this function. Indeed, if you only call this function, other variables such as the :attr:`time` will not be updated at the same time leading to future errors in plotting for instance.!! """ #import inspect #if inspect.stack()[1][3] != 'update': # import warnings # warnings.warn('You should not call Plant.update_counter # directly but via the update() method. See docstring.') self.counter.apices.append(lstring.count('A')) self.counter.internodes.append(lstring.count('I')) self.counter.leaves.append(lstring.count('L')) self.counter.branches.append(lstring.count('B')) self.counter.gus.append(lstring.count('U')) def update_DARC(self, D, R, C): """Update the :attr:`DARC` attribute given D, A, R, C values .. todo:: include A in the parametr list. .. warning:: you should use the :meth:`update` to call this function. Indeed, if you only call this function, other variables such as the :attr:`time` will not be updated at the same time leading to future errors in plotting for instance.!! """ self.DARC.D.append(D) self.DARC.R.append(R) self.DARC.C.append(C) def update(self, time_elapsed, lstring, fast=True, dvmin=0.1): """Main core of the plant modelling to cionpute the DARC values at each step plus the pipe model cost :param bool fast: if True, branch_update and growth_update are not called (save about 20% of CPU). calls :meth:`update_DARC` and :meth:`update_counter` methods .. todo:: this documentation """ self.age += self.dt self.mtgtools.set_order_path_rank() self.mtgtools.distance_to_apex_and_order_reassignment() #reset total demand self.D = 0. if self.options.misc.reset_resource: self.R = 0. #if self.Reserve > 0 and (self.age -self.year*365) < self.reserve_starting_time: # _reserve = min(2, self.Reserve) # self.R += _reserve # self.Reserve -= _reserve # reset total cost self.C = 0. self.dV = 0. for elt in lstring: if elt.name in ['R', 'A', 'I', 'L']: self.C += elt[0].livingcost self.D += elt[0].demandCalculation( context=self.options.context.model, order_coeff=self.options.context.order_coeff, height_coeff=self.options.context.height_coeff, rank_coeff=self.options.context.rank_coeff, d2a_coeff=self.options.context.d2a_coeff, vigor_coeff=self.options.context.vigor_coeff, age_coeff=self.options.context.age_coeff) self.R += elt[0].resourceCalculation() if elt.name in ['I']: self.dV += elt[0].dvolume / (1. / elt[0].cost_per_metamer) self.D *= self.dt self.R *= self.dt self.C *= self.dt # does not cost anything to check that dV is positive assert self.dV >= 0, self.dV self.update_DARC(self.D, self.R, self.C) # First sink processus:living cost ---------------------------------------------- # substract the living cost from the total resource. if self.C > self.R: self.R = 0. #self.Reserve -= self.C else: self.R -= self.C # Second sink: Reserve # the compute_reserve function should return a value less than R, so self.R must be >0 self.compute_reserve(alpha=self.reserve_alpha) # third sink is the pipe model --------------------------------------------------- self.variables.dV.append(self.dV) # let us compute the amount of dv that will be indeed allocated. # Given that we want to use at maximum the amount R*pipe_fraction. if self.R >= 0: dv_a = min(self.R * self.pipe_fraction, self.dV) assert dv_a >= 0 and dv_a <= self.R * self.pipe_fraction and dv_a <= self.dV else: dv_a = min(self.Reserve * self.pipe_fraction, self.dV) assert dv_a >= 0 and dv_a <= self.Reserve * self.pipe_fraction and dv_a <= self.dV # So, the pipe_ratio that is fulfilled if dv_a != 0: pipe_ratio = dv_a / self.dV else: pipe_ratio = 0. assert pipe_ratio >= 0 and pipe_ratio <= 1. self.DARC.pipe_cost.append(dv_a) cost_per_dv = 1. if self.R >= 0: self.R -= dv_a * cost_per_dv else: self.Reserve -= dv_a * cost_per_dv self.variables.pipe_ratio.append(pipe_ratio) #print 'new R=', self.R for elt in lstring: if elt.name in ['I']: # pipe_ratio**2 since v=2.pi r^2 elt[0].radius += (elt[0]._target_radius - elt[0].radius) * pipe_ratio**2. self._time.append(time_elapsed) if time_elapsed == 365: print '###################3' self.new_season() self.update_counter(lstring) self.branch_update(fast=fast) self.growth_unit_update(fast=fast) if self.D < 0. and self.D > -1e-15: self.D = 0 if self.A < 0. and self.A > -1e-15: self.A = 0 if self.R < 0. and self.R > -1e-15: self.R = 0 if self.C < 0. and self.C > -1e-15: self.C = 0 if self.D < 0 or self.A < 0 or self.R < 0 or self.C < 0: raise ValueError( "D (%s), A (%s), R (%s) and C (%s) cannot be negative!" % (self.D, self.A, self.R, self.C)) def compute_reserve(self, alpha=1): r"""Compute the reserve :param float alpha: a multiplier factor return the amount of resource to be allocated to the reserve at a given time .. math:: \textrm{Reserve}(t) = \alpha * f(t) * R where :math:`R` is the total resource of the system, and :math:`f(t)` a sigmoid function starting at a given **starting_time**. The parameter of te sigmoid function can be given when instanciating the :class:`Plant` object """ # compute the status of the reserve function (sigmoid between 0-1) _reserve_fraction = alpha * self._reserve_function.growthValue( self.age - self.reserve_starting_time - 365 * (self.year - 1)) # a user argument can decrease the max value by multiplying by an alpha parameter. _reserve = _reserve_fraction * self.R # so, the reserve at this time step is: self.Reserve += _reserve self.R -= _reserve self.variables.reserve.append(_reserve) def new_season(self): self.R = min(self.Reserve, 10) #self.Reserve = 0. self.year += 1 def growth_unit_update(self, fast=True): """update growth unit information. #. recompute the number of internode unit in a branch #. recompute the branch length #. recompute the branch base radius (e.g., first internode radius) """ if fast == True: return gu_ids = self.mtgtools.ids['U'] """ #tool long with the DB because it is created at each time step.... self.mtgtools.createDB() self.mtgtools.connectDB() for gu_id in gu_ids: counter = len(self.mtgtools.select(select="id", label='I', complex=gu_id)) self.mtg.property('GrowthUnit')[gu_id].internode_counter = counter length = sum(self.mtgtools.select_attribute(attribute="length", select="id", label='I', complex=gu_id)) self.mtgtools.mtg.property('GrowthUnit')[gu_id].length = length radius = self.mtgtools.select_attribute(attribute="radius", select="id", label='I', complex=gu_id) if len(radius) != 0: self.mtgtools.mtg.property('GrowthUnit')[gu_id].radius = max(radius) self.mtgtools.closeDB() """ # save the number of internodes in each GU using standard mtg code. for vid in gu_ids: counter = len([ id for id in list(self.mtg.components_at_scale(vid, 4)) if self.mtg.class_name(id) == 'I' ]) self.mtg.property('GrowthUnit')[vid].internode_counter = counter # computes the length for vid in gu_ids: length = sum([ self.mtg.property('Internode')[id].length for id in list(self.mtg.components_at_scale(vid, 4)) if self.mtg.class_name(id) == 'I' ]) self.mtg.property('GrowthUnit')[vid].length = length for vid in gu_ids: ids = self.mtg.components_at_scale_iter(vid, scale=4) try: first_id = ids.next() if self.mtg.class_name(first_id) == 'I': self.mtg.property('GrowthUnit')[vid].radius = \ self.mtg.property('Internode')[first_id].radius except: # this GU is probably empty pass def branch_update(self, fast=True): """update branch information. #. recompute the number of growth unit in a branch #. recompute the number of internode unit in a branch #. recompute the branch length #. recompute the branch base radius (e.g., first internode radius) """ if fast == True: return from openalea.mtg.aml import Activate, Components, Class, VtxList Activate(self.mtg) try: branch_ids = VtxList(2) if len(branch_ids) == 0: return except: return for vid in branch_ids: counter = len([ id for id in list(self.mtg.components_at_scale(vid, 4)) if self.mtg.class_name(id) == 'I' ]) self.mtg.property('Branch')[vid].internode_counter = counter counter = len([ id for id in list(self.mtg.components_at_scale(vid, 3)) if self.mtg.class_name(id) == 'U' ]) self.mtg.property('Branch')[vid].growthunit_counter = counter # calculate the branches total length internode_ids = [Components(x, Scale=4) for x in branch_ids] length = [[ sum([ self.mtg.property('Internode')[id].length for id in y if self.mtg.class_name(id) == 'I' ]) ] for y in internode_ids] for vid, length in zip(branch_ids, length): self.mtg.property('Branch')[vid].length = length[0] #compute the branches radius for vid in branch_ids: try: first_id = self.mtg.components_at_scale_iter(vid, scale=4).next() if self.mtg.class_name(first_id) == 'I': self.mtg.property( 'Branch')[vid].radius = self.mtg.property( 'Internode')[first_id].radius except: #this is probably an empty branch pass #else: # self.mtg.property('Branch')[vid].radius = 0. def _get_options(self): return self._options options = property(fget=_get_options, doc="getter to the options form the configuration file") def _get_revision(self): return self._revision revision = property( fget=_get_revision, doc= "getter to the SVN :attr:`revision` of the lsystem used within the simulation" ) def _get_dt(self): return self._dt dt = property(fget=_get_dt, doc="getter to time step :attr:`dt`") revision = property( fget=_get_revision, doc="the revision of the lsystem used within the simulation") def _get_mtg(self): return self.mtgtools.mtg mtg = property( fget=_get_mtg, doc= "getter/alias to :attr:`mtgtools`.mtg. to set it, use :attr:`mtgtools`.mtg instead" ) def _get_time(self): return self._time time = property(fget=_get_time, doc="getter for the time array") def _get_filename(self): return self._filename filename = property(fget=_get_filename, fset=None, doc="getter to prefix filename") def _set_filename(self, ifilename, tag): """convert ifilename and tag into a filename string without extension""" # set the filename if ifilename == None: filename = 'plant' else: filename = ifilename # add a tag if tag != None: filename += '_' + tag return filename
class Leaf(ComponentInterface): r"""Specialised version of :class:`~openalea.plantik.biotik.component.ComponentInterface` dedicated to Leaves. .. warning:: If the parameter store_data is True, then the :attr:`variables` attributes will store the area, resource and age at each time step, which may be costly. It is set to false by default. :Example: >>> from vplants.plantik.biotik.leaf import * >>> i = Leaf(store_data=True, resource_per_day=1) >>> i.age.days 0 >>> i.variables.age.values [0] :Notation: * age of a leaf is denoted :math:`t_a^{(l)}` .. csv-table:: **Notation related to :class:`~openalea.plantik.biotik.leaf.Leaf`** :header: Name, symbol, default value :widths: 15,20,20 optimal resource , :math:`r_0^{(l)}`, area , :math:`\mathcal{A}(t_a)` , a user :class:`~openalea.plantik.biotik.growth.GrowthFunction` petiole_radius , :math:`r_p` , 0.0005 meters area min , :math:`S_{min}` , 1. 10e-4 square meter area max , :math:`S_{max}` , 30. 10e-4 square meter mass per area , :math:`a` , 200 g/m^2 g per square meter efficiency , ":math:`\mathcal{E}(t,t_a)`" , see :meth:`leaf_efficiency` efficiency parameter, ":math:`t_1`" , see :meth:`leaf_efficiency` efficiency parameter, :math:`t_2`" , see :meth:`leaf_efficiency` growth rate , :math:`\lambda^{(l)}` , 1 nu (logistic) , :math:`\nu` , 1 maturation , :math:`T_m^{(l)}` , 21 days .. todo:: move area_min, area_max, mass_per_area as arguments """ petiole_radius = 0.0005 area_min = 1. * 0.01 * 0.01 # 1 cm^2 changed into m^2 area_max = 30 * 0.01 * 0.01 mass_per_area = 220 # g/m^2 def __init__(self, resource_per_day, birthdate=None, id=None, maturation=21, internode_vigor=1., livingcost=0., growth_rate=1, area_optimal=30 * 0.01 * 0.01, growth_function='sigmoid', efficiency_method='unity', store_data=False, nu=1, t1=15, t2=150, angle=0.): """**Constructor** :param float resource_per_day: stricly positive , notation :math:`r_0`. :param datetime.datetime birthdate: :param int id: :param float maturation: leaf maturation in days :param float internode_vigor: a leaf area is proportional to the internode length. intenode_vigor of 1 means the internode had full length and therefore leaf has a potential for maximum area as well. :param float livingcost: zero by default :param float growth_rate: the :math:`lambda` parameter of the growth function :param str growth_function: a GrowthFunction method in ['linear', 'sigmoid', 'logistic'] :param nu: shape of the logistic function is :attr:`growth_function` is logisitic :param efficiency_method: 'unity' or 'sigmoid' (default is unity) :param int store_data: :param float t1: parameter of the leaf_efficiency method :param float t2: parameter of the leaf_efficiency method :attributes: * :attr:`area`: the leaf area, a :meth:`~vplants.plantik.biotik.growth.GrowthFunction` instance (read-only). The input parameters **growth_function**, **growth_rate**, **nu**, and **maturation** are used by this function. * Those inherited by :class:`~vplants.plantik.biotik.component.ComponentInterface`: :attr:`age`, :attr:`allocated`, :attr:`demand`, :attr:`livingcost`, :attr:`~vplants.plantik.biotik.component.ComponentInterface.resource`. * :attr:`variables` is a :class:`CollectionVariables` instance containing the :attr:`age`, :attr:`demand`, :attr:`resource`, :attr:`area`. """ assert internode_vigor >= 0 and internode_vigor <= 1, \ "internode vifor must be in [0,1]" assert resource_per_day > 0, \ 'resource_per_day must be positive otherwise noting will happen...' self.context = Context() ComponentInterface.__init__(self, label='Leaf', birthdate=birthdate, id=id, state='growing') # leaf min must correspond to internode length min so that # leafmaxarea>-leafminarea self._radius = Leaf.petiole_radius # for geometry purpose only # set the area growth function self.area_max = Leaf.area_max * internode_vigor self._area = GrowthFunction(Leaf.area_min, self.area_max, maturation=maturation, growth_rate=growth_rate, growth_function=growth_function, nu=nu) self._r0 = resource_per_day self.angle = angle # used for bookeeping only self.internode_vigor = internode_vigor # in % self.maturation = maturation # in days # used by update() self.store_data = store_data # some variables to store. self.variables = CollectionVariables() self.variables.add( SingleVariable(name='age', unit='days', values=[self.age.days])) self.variables.add( SingleVariable(name='resource', unit='biomass unit', values=[self.resource])) self.variables.add( SingleVariable(name='demand', unit='biomass unit', values=[self.demand])) self.variables.add( SingleVariable(name='area', unit='square meters', values=[self.area])) # parameters self.livingcost = livingcost # cost to maintain leaf alive !! self.efficiency_method = efficiency_method # other attributes. # radius used by the interpretation of the lsystem. #todo: move it elsewhere outsitde this class. self.father_radius = 0 #others self.lg = 0. # light interception # related to leaf efficiency self._leaf_efficiency = 1. self.t1 = t1 self.t2 = t2 self.growth_rate = growth_rate def update(self, dt): """Update function This update function performs the following tasks: #. increment age by dt #. calls :meth:`demandCalculation`, :meth:`resourceCalculation` and :meth:`computeLivingCost` #. store age into variables.age #. store resource into variables.resource #. store demand into variables.demand #. store area into variables.area if age< maturation :param float dt: in days """ super(Leaf, self).update(dt) self.demandCalculation() self.resourceCalculation() self.computeLivingcost() if self.store_data is True: self.variables.age.append(self.age.days) self.variables.resource.append(self.resource) self.variables.demand.append(self.demand) if self.age.days < self.maturation: self.variables.area.append(self.area) def __str__(self): res = super(Leaf, self).__str__() res += self.context.__str__() res += self.variables.__str__() res += title('other leaf attributes') res += 'area = %s\n' % self.area res += 'leaf_efficiency = %s\n' % self.leaf_efficiency res += 'r0 = %s\n' % self.r0 res += 'radius = %s\n' % self.radius res += 'area_max = %s\n' % self.area_max res += 'efficiency_method = %s\n' % self.efficiency_method res += 'father_radius = %s\n' % self.father_radius res += 'internode_vigor = %s\n' % self.internode_vigor res += 'lg = %s\n' % self.lg res += 't1 = %s\n' % self.t1 res += 't2 = %s\n' % self.t2 res += 'maturation = %s\n' % self.maturation res += 'store_data = %s\n' % self.store_data res += self._area.__str__() return res def demandCalculation(self, **kargs): """leaf has no demand""" self.demand = 0. return self.demand def resourceCalculation(self): r"""leaf resource is computed as follows: .. math:: r(t,t_a) = r_0 \frac{ \mathcal{A}(t_a) }{ A_{\textrm{max}}} \mathcal{E}(t,t_a) where :math:`\mathcal{E}(t)` is the leaf efficiency, :math:`\mathcal{A}`, the leaf area and :math:`\mathcal{A}_{\textrm{max}}` the area of a standard leaf and :math:`r_0` the resource of a standard leaf per day.. See :meth:`~vplants.plantik.biotik.leaf.leaf_efficiency` method. Since :math:`r_0` is a unit of resource per day, :math:`r(t)` is in unit of biomass per day """ self.resource = self.r0 * self.area / Leaf.area_max self.resource *= self.leaf_efficiency() return self.resource def leaf_efficiency(self): r"""simple senescence method made of two sigmoid of parameter t1, t2 There are currently two methods: unity and sigmoid. The former returns 1 whatsoever, while the latter returns the following quantity: .. math:: \frac{1}{\left(1+\exp^{\lambda (t_1 - t_a)}\right) \left(1+\exp^{\lambda (t_a - t_2)}\right)} where :math:`t_1` and :math:`t_2` are the sigmoids parameter and :math:`\lambda` is the growth rate parameter. .. plot:: :include-source: from pylab import * from vplants.plantik.biotik.leaf import * b = Leaf(resource_per_day=1, store_data=True, efficiency_method='sigmoid') for v in range(1,200): b.update(1) b.plot('resource') grid(True) axvline(15,linewidth=3, color='red', alpha=0.5) axvline(150,linewidth=3, color='red', alpha=0.5) """ if self.efficiency_method == 'unity': return 1. elif self.efficiency_method in ['logistic', 'sigmoid']: from pylab import exp return 1./(1+exp(self.growth_rate*(self.t1-self.age.days))) /\ (1.+exp(self.growth_rate*(self.age.days-self.t2))) else: raise ValueError( 'efficiency_method must be either unity or sigmoid (config.ini file)' ) def computeLivingcost(self): return self.livingcost def _getMass(self): return Leaf.mass_per_area * self.area mass = property(_getMass, None, None, doc="returns the leaf mass in g per m^2") # the petiole radius does not change, so no setter def _getRadius(self): return self._radius radius = property(_getRadius, None, None, doc="petiole radius") def _getArea(self): return self._area.growthValue(self.age.days) area = property(_getArea, None, None, doc="getter to the leaf area.") def _getR0(self): return self._r0 r0 = property(_getR0, None, None, doc="optimal resource per day") def plot(self, variables=['demand', 'resource', 'area'], show=True, grid=True, **args): """plot demand, resource and area over time :param list variables: plot results related to the variables provided :param bool show: create but do not show the plot (useful for test, saving) :param args: any parameters that pylab.plot would accept. .. plot:: :include-source: from vplants.plantik.biotik.leaf import * b = Leaf(resource_per_day=1, store_data=True) for v in range(1,30): b.update(1) b.plot() """ self.variables.plot(variables=variables, show=show, grid=grid, **args)
def __init__(self, final_length=0.03, length_max=0.03, length_min=0.001, cambial_fraction=0., birthdate=None, id=None, maturation=10, radius_min=0.001, growth_rate=1, growth_function='logistic', store_data=False, nu=1): r"""**Constructor** :param float final_length: final length :math:`l^{(i)_{\rm{final}}}` :param float length_max: maximum internode length, :math:`l^{(i)}_{max}` (default is 3cm) :param float length_min: maximum internode length, :math:`l^{(i)}_{min}` (default is 0.1cm) :param float cambial_fraction: :math:`p_c` (default is 0.) :param datetime.datetime birthdate: :param int id: :param float maturation: :math:`T^{(i)}_m` (default is 10) :param float radius_min: starting radius of an internode, :math:`r^{(i)}_{min}` (in meters) (default is 0.001) :param str growth_function: linear or logistic (default is logistic) :param float growth_rate: :math:`\lambda` (default is 1) :param float nu: :math:`\nu^{(i)}` parameter of the logistic function (default is 1) :param int store_data: store length, radius, demand at each time step (default is False) .. note:: the growth function is logistic by default, which is identical to a sigmoid function isnce :math:`\nu=1` and :math:`\lambda=1`. :attributes: * :attr:`length`: internode length * :attr:`radius`: internode radius (supposed the same from base to top) * :attr:`target_radius`: at each time step, a pipe model may be computed indicating what the new radius should be. * those inherited by :class:`~vplants.plantik.biotik.component.ComponentInterface`: * :attr:`age` * :attr:`demand` * :attr:`birthdate`, ... * :attr:`mass` * :attr:`volume` """ self.radius_min = radius_min self.length_max = length_max self.length_min = length_min self.final_length = final_length assert final_length >= length_min, 'final length must be greater or equal to min length' assert length_max >= length_min, 'max length must be greater or equal to min length' self.volume_standard = 3.14159 * self.radius_min**2 * self.length_max self.cost_per_metamer = 1. / (self.radius_min * self.radius_min * self.length_max * pi) self.context = Context() ComponentInterface.__init__(self, label='Internode', birthdate=birthdate, id=id) self._radius = self.radius_min self._target_radius = self.radius_min self._length = GrowthFunction(self.length_min, self.final_length, maturation=maturation, growth_rate=growth_rate, nu=nu, growth_function=growth_function) self.store_data = store_data self.variables = CollectionVariables() self.variables.add( SingleVariable(name='age', unit='days', values=[self.age.days])) self.variables.add( SingleVariable(name='length', unit='meters', values=[self.length])) self.variables.add( SingleVariable(name='radius', unit='meters', values=[self.radius])) #self.variables.add(SingleVariable(name='demand', # unit='biomass unit', values=[self.demand])) self.variables.add( SingleVariable(name='living_cost', unit='biomass unit', values=[self.livingcost])) self.demand_coeff = 0. self.density = 1. assert cambial_fraction <= 1 and cambial_fraction >= 0 self._mu = cambial_fraction # % of cambial layer alive
class Internode(ComponentInterface): r"""Internode. Specialised version of :class:`~openalea.plantik.biotik.component.ComponentInterface` :Example: >>> from vplants.plantik.biotik.internode import * >>> i = Internode(store_data=True, growth_rate=1) >>> i.radius 0.001 Use the :meth:`update` to increment the age of a component. This method will take care of updating relevant properties such as the internode length. :plotting: If the argument store_data is set to True, attributes such as length are stored over time in the :attr:`variables`, which you can plot using either the :meth:`plot` method or from the variables stored in :attr:`variables`. .. seealso:: :mod:`~openalea.plantik.biotik.collection` module :Notation: * the age of the internode is denoted :math:`t_a` .. csv-table:: **Notation related to** :class:`~openalea.plantik.biotik.internode.Internode` :header: Name, symbol, default value :widths: 15,20,20 radius_min , :math:`r^{(i)}_{min}` , 0.001 meters radius , :math:`r^{(i)}` , output radius target , :math:`r^{(i)}_{target}` , internal final length , :math:`l^{(i)}` , user input length , :math:`l^{(i)}` , output length_min , :math:`l^{(i)}_{min}` , 0.001 meters length_max , :math:`l^{(i)}_{max}` , 0.03 meters maturation , :math:`T^{(i)}_m` , 10 days growth rate , :math:`\lambda^{(i)}` , 1 nu (logistic) , :math:`\nu^{(i)}` , 1 cambial fraction , :math:`p_c` , 0 % volume , :math:`V^{(i)}` , output density , :math:`\rho^{(i)}` , internal mass , :math:`m^{(i)}` , output """ def __init__(self, final_length=0.03, length_max=0.03, length_min=0.001, cambial_fraction=0., birthdate=None, id=None, maturation=10, radius_min=0.001, growth_rate=1, growth_function='logistic', store_data=False, nu=1): r"""**Constructor** :param float final_length: final length :math:`l^{(i)_{\rm{final}}}` :param float length_max: maximum internode length, :math:`l^{(i)}_{max}` (default is 3cm) :param float length_min: maximum internode length, :math:`l^{(i)}_{min}` (default is 0.1cm) :param float cambial_fraction: :math:`p_c` (default is 0.) :param datetime.datetime birthdate: :param int id: :param float maturation: :math:`T^{(i)}_m` (default is 10) :param float radius_min: starting radius of an internode, :math:`r^{(i)}_{min}` (in meters) (default is 0.001) :param str growth_function: linear or logistic (default is logistic) :param float growth_rate: :math:`\lambda` (default is 1) :param float nu: :math:`\nu^{(i)}` parameter of the logistic function (default is 1) :param int store_data: store length, radius, demand at each time step (default is False) .. note:: the growth function is logistic by default, which is identical to a sigmoid function isnce :math:`\nu=1` and :math:`\lambda=1`. :attributes: * :attr:`length`: internode length * :attr:`radius`: internode radius (supposed the same from base to top) * :attr:`target_radius`: at each time step, a pipe model may be computed indicating what the new radius should be. * those inherited by :class:`~vplants.plantik.biotik.component.ComponentInterface`: * :attr:`age` * :attr:`demand` * :attr:`birthdate`, ... * :attr:`mass` * :attr:`volume` """ self.radius_min = radius_min self.length_max = length_max self.length_min = length_min self.final_length = final_length assert final_length >= length_min, 'final length must be greater or equal to min length' assert length_max >= length_min, 'max length must be greater or equal to min length' self.volume_standard = 3.14159 * self.radius_min**2 * self.length_max self.cost_per_metamer = 1. / (self.radius_min * self.radius_min * self.length_max * pi) self.context = Context() ComponentInterface.__init__(self, label='Internode', birthdate=birthdate, id=id) self._radius = self.radius_min self._target_radius = self.radius_min self._length = GrowthFunction(self.length_min, self.final_length, maturation=maturation, growth_rate=growth_rate, nu=nu, growth_function=growth_function) self.store_data = store_data self.variables = CollectionVariables() self.variables.add( SingleVariable(name='age', unit='days', values=[self.age.days])) self.variables.add( SingleVariable(name='length', unit='meters', values=[self.length])) self.variables.add( SingleVariable(name='radius', unit='meters', values=[self.radius])) #self.variables.add(SingleVariable(name='demand', # unit='biomass unit', values=[self.demand])) self.variables.add( SingleVariable(name='living_cost', unit='biomass unit', values=[self.livingcost])) self.demand_coeff = 0. self.density = 1. assert cambial_fraction <= 1 and cambial_fraction >= 0 self._mu = cambial_fraction # % of cambial layer alive def __str__(self): res = super(Internode, self).__str__() res += self.context.__str__() res += self.variables.__str__() res += title('other attributes') return res def resourceCalculation(self): """No resource in an internode. For now, it is not used (r=0) """ self.resource = 0. return self.resource def demandCalculation(self, **kargs): """demand of the internode For now, this is not used (i.e., d=0) """ #demand_coeff is zero, so d=0 self.demand = self.volume * self.demand_coeff return self.demand def _getVolume(self): """Returns total internode volume .. math:: \pi r^2 \\times l^{(i)} """ return pi * self.radius * self.radius * self.length volume = property(_getVolume, None, None) def _getdVolume(self): r"""Retuns the volume that is not yet built .. seealso:: :attr:`target_radius` .. math:: dV = \pi l^{(i)} \left( r_{\rm{target}}^2 - r^2 \right) """ return pi * (self._target_radius * self._target_radius - self.radius * self.radius) * self.length dvolume = property(_getdVolume, None, None) def _getMass(self): r"""returns the mass of the internode .. math:: m^{(i)} = V^{(i)} \rho^{(i)} """ return self.density * self.volume mass = property(_getMass, None, None) def _getRadius(self): return self._radius def _setRadius(self, radius): if radius < self._radius: raise ValueError("radius decreased in internode !!") self._radius = radius radius = property(_getRadius, _setRadius, None, doc="internode radius. Cannot decrease !") def _getTargetRadius(self): return self._target_radius def _setTargetRadius(self, target_radius): self._target_radius = target_radius target_radius = property(_getTargetRadius, _setTargetRadius, None, doc=r""" Target radius :math:`r_{\rm{target}}` that the internode tend to reach This is used by the pipe model. """) def _getLength(self): return self._length.growthValue(self.age.days) length = property(_getLength, None, None, doc="internode length :math:`l^{(i)}`") def update(self, dt): """Update function This update function performs the following tasks: #. increment age by dt #. calls :meth:`demandCalculation`, :meth:`resourceCalculation` and :meth:`computeLivingCost` #. store age :attr:`variables` #. store length in :attr:`variables` #. store radius in :attr:`variables` :param float dt: .. note:: when maturation is reached, length is not stored any more """ super(Internode, self).update(dt) self._compute_livingcost(dt) self.demandCalculation() self.resourceCalculation() if self.store_data is True: self.variables.age.append(self.age.days) # once maturation is reached, there is not point is storing the # length, which ha reached a maximum if self.age.days <= self._length.maturation: self.variables.length.append(self.length) self.variables.radius.append(self.radius) def _compute_livingcost(self, dt): """ Within an internode, only the external layer requires a livingcost. The external layer is comprise between the max radisu R and the min raidus r. So, the cambial volume is equivalent to the volume of an empty cylinder This is a volume that equals :math:`\pi h R^2 (2\mu -\mu^2)`. where `\mu` is a fraction of R """ self.livingcost = self.volume * self._mu * (2. - self._mu) self.livingcost *= self.cost_per_metamer * dt def plot(self, variables=['radius', 'length', 'living_cost'], show=True, grid=True, **args): """plot radius, length and living cost over time :param list variables: plot results related to the variables provided :param bool show: create but do not show the plot (useful for test, saving) :param args: any parameters that pylab.plot would accept. .. plot:: :include-source: from pylab import * from vplants.plantik.biotik.internode import Internode i = Internode(store_data=True) for v in range(1,30): i.update(1) i.plot('length') .. note:: Although, the iteration went over 30 days, the plot shows only 10 days because the internode reaced maturity after 10 days. """ self.variables.plot(variables=variables, show=show, grid=grid, **args)
def __init__(self, birthdate=None, demand=2, metamer_cost=2, livingcost=0., height=0., id=0, plastochron=3., growth_threshold = 0.2, vigor=0.1, store_data=False): """**Constructor** :param datetime.datetime birthdate: (default is None) :param float demand: (default is 2) :param float metamer_cost: (default is 2) :param float livingcost: (default is 0) :param float height: (default is 0) :param int id: (default is 0) :param plastochron: (default is 3) :param float growth_threshold: a value between 0 and 1 allowing a production once allocation is larger than this value :param vigor: (default is 0.1) :param store_data: used by :meth:`save_data_product` to save data at each time step (default is False). Additional attributes :attributes: * :attr:`current_plastochron`: * :attr:`radius`: (default is 0) * :attr:`growth_potential`: (default is 1) * :attr:`interruption`: time step during which an apex is not growing (default is 0.) * :attr:`growing` (default is False) * :attr:`father_radius` (default is 0.) * :attr:`lg`: used to store light interception * :attr:`internodes_created` keep track of the number of internodes created by this apex (default is 0.). """ self.context = Context() ComponentInterface.__init__(self, label='Apex', birthdate=birthdate, id=id) # read-only attribute self.plastochron = plastochron # time interval at which production of new biological object is possible self.metamer_cost = metamer_cost # cost to produce new object (internode + new bud) self.demand = demand # demand of the metamer in biomass unit/per time unit self.store_data = store_data self._height = height self._vigor = vigor self._growth_threshold = growth_threshold self.lg = 0.14 self.variables = CollectionVariables() self.variables.add(SingleVariable(name='age', unit='days', values=[self.age.days])) self.variables.add(SingleVariable(name='height', unit='meters', values=[self.height])) self.variables.add(SingleVariable(name='demand', unit='biomass unit', values=[self.demand])) self.variables.add(SingleVariable(name='allocated', unit='biomass unit', values=[self.allocated])) self.variables.add(SingleVariable(name='vigor', unit='biomass unit', values=[self.vigor])) self.variables.add(SingleVariable(name='lg', unit='arbitrary', values=[self.lg])) #read-write attributes self.livingcost = livingcost # cost to maintain the apex alive self.initial_demand = demand # additional attributes self.current_plastochron = 0. # keep track of the plastochron self.radius = 0.00 # for the pipe model self.growth_potential = 1 #? self.father_radius = 0. self.interruption = 0. # time step during which an apex is not growing self.growing = False self.internodes_created = 0. # count number of internodes created by this apex self.lg = 0.14 self.type = 'Apex' #apex or meristem
class Apex(ComponentInterface): r"""class dedicated to apex component. An apex is a biological objects that can produce other biological objects. As a biological component, it inherits attributes and methods from :class:`~vplants.plantik.biotik.component.ComponentInterface`. :Example: >>> from vplants.plantik.biotik.apex import Apex >>> i = Apex(store_data=True) >>> i.age.days 0 :Notation: * age of an apex is denoted :math:`t_a` .. csv-table:: **Notation related to** :class:`~openalea.plantik.biotik.apex.Apex` :header: Name, symbol, default value :widths: 15,20,20 optimal demand , :math:`d^{(a)_0}` , 2 UR plastochron , :math:`T^{(a)}_p` , 3 days growth threshold , :math:`\lambda` , 0.2 metamer cost , :math:`m_{\rm cost}` , 2 UR living cost ,- , 0 vigor ,- , 0.1 """ def __init__(self, birthdate=None, demand=2, metamer_cost=2, livingcost=0., height=0., id=0, plastochron=3., growth_threshold = 0.2, vigor=0.1, store_data=False): """**Constructor** :param datetime.datetime birthdate: (default is None) :param float demand: (default is 2) :param float metamer_cost: (default is 2) :param float livingcost: (default is 0) :param float height: (default is 0) :param int id: (default is 0) :param plastochron: (default is 3) :param float growth_threshold: a value between 0 and 1 allowing a production once allocation is larger than this value :param vigor: (default is 0.1) :param store_data: used by :meth:`save_data_product` to save data at each time step (default is False). Additional attributes :attributes: * :attr:`current_plastochron`: * :attr:`radius`: (default is 0) * :attr:`growth_potential`: (default is 1) * :attr:`interruption`: time step during which an apex is not growing (default is 0.) * :attr:`growing` (default is False) * :attr:`father_radius` (default is 0.) * :attr:`lg`: used to store light interception * :attr:`internodes_created` keep track of the number of internodes created by this apex (default is 0.). """ self.context = Context() ComponentInterface.__init__(self, label='Apex', birthdate=birthdate, id=id) # read-only attribute self.plastochron = plastochron # time interval at which production of new biological object is possible self.metamer_cost = metamer_cost # cost to produce new object (internode + new bud) self.demand = demand # demand of the metamer in biomass unit/per time unit self.store_data = store_data self._height = height self._vigor = vigor self._growth_threshold = growth_threshold self.lg = 0.14 self.variables = CollectionVariables() self.variables.add(SingleVariable(name='age', unit='days', values=[self.age.days])) self.variables.add(SingleVariable(name='height', unit='meters', values=[self.height])) self.variables.add(SingleVariable(name='demand', unit='biomass unit', values=[self.demand])) self.variables.add(SingleVariable(name='allocated', unit='biomass unit', values=[self.allocated])) self.variables.add(SingleVariable(name='vigor', unit='biomass unit', values=[self.vigor])) self.variables.add(SingleVariable(name='lg', unit='arbitrary', values=[self.lg])) #read-write attributes self.livingcost = livingcost # cost to maintain the apex alive self.initial_demand = demand # additional attributes self.current_plastochron = 0. # keep track of the plastochron self.radius = 0.00 # for the pipe model self.growth_potential = 1 #? self.father_radius = 0. self.interruption = 0. # time step during which an apex is not growing self.growing = False self.internodes_created = 0. # count number of internodes created by this apex self.lg = 0.14 self.type = 'Apex' #apex or meristem def _getHeight(self): return self._height def _setHeight(self, height): self._height = height height = property(_getHeight, _setHeight, None, doc="getter/setter to distance between apex and root") def _getGrowthThreshold(self): return self._growth_threshold growth_threshold = property(_getGrowthThreshold, None, None, doc="getter to growth threshold") def _getVigor(self): return self._vigor def _setVigor(self, vigor): self._vigor = vigor vigor = property(_getVigor, _setVigor, None, doc="getter/setter to distance between apex and root") def update(self, dt): """Update function This update function performs the following tasks: #. increment age by dt #. increment plastochron by dt #. increment interruption time by dt #. calls :meth:`demandCalculation`, :meth:`resourceCalculation` and :meth:`computeLivingCost` #. store age into variables.age #. store height into variables.height #. store demand into variables.demand #. store allocated into variables.allocated #. store vigor into variables.vigor #. store d2a into variables.d2a :param float dt: """ super(Apex, self).update(dt) self.current_plastochron += dt self.interruption += dt if self.store_data is True: self.variables.age.append(self.age.days) self.variables.height.append(self.height) self.variables.demand.append(self.demand) self.variables.allocated.append(self.allocated) self.variables.vigor.append(self.vigor) self.variables.lg.append(self.lg) """if self.allocated > 0 and self.current_plastochron==self.plastochron: if self.growing == True: self.vigor += 0.05 if self.vigor>=1: self.vigor = 1. else: self.vigor -=0.05 if self.vigor <=0: self.vigor =0.05 """ if self.current_plastochron == self.plastochron: #check if the apex got some resource even tough it does not grow. if self.allocated>0: self.vigor += 0.05 if self.vigor>=1: self.vigor = 1. else: self.vigor -=0.05 if self.vigor <=0: self.vigor =0.05 def demandCalculation(self, **kargs): r"""Compute the demand of an apex according to its context :param float context: order_height :math:`\alpha_o` :param float context: height_height :math:`\alpha_h` :param float context: rank_height :math:`\alpha_r` :param float context: age_height :math:`\alpha_a` :param float context: vigor_height :math:`\alpha_v` The order is denoted :math:`o`. The height is :math`h`. The rank is :math`r`. The age is :math`a`. The satisfaction is :math`v`. .. math:: d = d_0 \mathcal{O} \mathcal{H} \mathcal{R} \mathcal{A} \mathcal{V} .. math:: \mathcal{O} = \frac{1}{(1 + o)^{\alpha_o}} .. math:: \mathcal{H} = \frac{1}{(1+h)^{\alpha_h}} .. math:: \mathcal{R} = \frac{1}{(1+r)^{\alpha_r}} .. math:: \mathcal{A} = (\frac{1}{1+exp^{+(0.03*(age-90.))}})^{\alpha_a} .. math:: \mathcal{V} = (vigor)^{\alpha_v} """ order_coeff = kargs.get("order_coeff", 0) height_coeff = kargs.get("height_coeff", 0) rank_coeff = kargs.get("rank_coeff", 0) age_coeff = kargs.get("age_coeff", 0) vigor_coeff = kargs.get("vigor_coeff", 0) d2a_coeff = kargs.get("d2a_coeff", 0) context = kargs.get("context", "order_height") #todo refactoering switch model to context model = context assert model in ["none", "order_height", "additive", "multiplicative"],\ 'check your config.ini file (model field) %s provided ' % model order = self.context.order height = self.context.height rank = self.context.rank d2a = self.context.d2a if model=="order_height": self.demand = self.initial_demand / float(order+1)**order_coeff / float(height)**height_coeff return self.demand elif model=='none': # nothing to be done in the simple model return self.initial_demand elif model =='additive' or model == 'multiplicative': self.demand = self.initial_demand weight = self.context.get_context_weight(model=model, order_coeff=order_coeff, height_coeff=height_coeff, rank_coeff=rank_coeff, d2a_coeff=d2a_coeff) #if age_coeff>=0: w1 = (2 - 2./(1.+exp(-age_coeff * self.age.days))) #else: # w1 = 2./(1.+exp(age_coeff*self.age.days))-1. #assert w1<=1 #if vigor_coeff>=0: w2 = (2 - 2./(1.+exp(-vigor_coeff * self.age.days))) #else: # w2 = 2./(1.+exp(vigor_coeff*self.age.days))-1. #assert w2<=1 if model == 'additive': weight = (weight +w1 +w2)/12. assert weight >=0 and weight<=1. elif model == 'multiplicative': weight = weight * w1 * w2 assert weight >=0 and weight<=1. self.demand = self.initial_demand * (weight) self.demand *= min(1., self.lg/0.04) return self.demand def computeLivingcost(self): pass def resourceCalculation(self): """Apices returns resource equals zero""" assert self.resource == 0, "how come apex have some resource ? " return self.resource def plot(self, clf=True, show=True, symbol='-o'): """Plot internal state variables such as demand and allocated resource .. plot:: from pylab import * from openalea.plantik.biotik.apex import Apex a = Apex(store_data=True) [a.update(1) for x in range(10)] a.plot() """ import pylab height = self.variables.height.plot(show=False)[0] demand = self.variables.demand.plot(show=False)[0] allocated = self.variables.allocated.plot(show=False)[0] vigor = self.variables.vigor.plot(show=False)[0] pylab.plot(height.get_xdata(), height.get_ydata(), marker='o', color='b', label='Height') pylab.hold(True) pylab.plot(demand.get_xdata(), demand.get_ydata(), marker='o', color='g', label='Demand') pylab.plot(allocated.get_xdata(), allocated.get_ydata(), marker='o', color='r', label='allocated') pylab.plot(vigor.get_xdata(), vigor.get_ydata(), marker='o', color='c', label='vigor') pylab.plot([min(self.variables.age.values), max(self.variables.age.values)], [self.growth_threshold, self.growth_threshold], color='m', label='threshold') pylab.legend() if show is True: pylab.show() def __str__(self): res = super(Apex, self).__str__() res += self.context.__str__() res += self.variables.__str__() res += title('other apex attributes. to be done') return res def plot_variables(self, variables=['demand', 'allocated'], show=True, grid=True, **args): """plot some results :param list variables: plot results related to the variables provided :param bool show: show plot or not (default is True). Useful for testing, saving :param bool grid: set grid on or off (default True) :param args: any parameters that pylab.plot would accept. """ self.variables.plot(variables=variables, show=show, grid=grid, **args)