Source code for openalea.cnwgrass.hydraulics.simulation

"""
    hydraulics.simulation
    ~~~~~~~~~~~~~~~~~~

    The module :mod:`hydraulics.simulation` is the front-end to run the model Hydraulics.
    The public API consists of methods :meth:`initialize` and :meth:`run`.

"""

from __future__ import division  # use "//" to do integer division
import logging

import numpy as np
from scipy.integrate import solve_ivp
from scipy import interpolate

from openalea.cnwgrass.hydraulics import model, parameters


[docs] class SimulationError(Exception): """ Abstract class for the management of simulation errors. Do not instance it directly. """ pass
[docs] class SimulationConstructionError(SimulationError): """ Exception raised when a problem occurs in the constructor, in particular when the arguments are not consistent with each other. """ pass
[docs] class SimulationInitializationError(SimulationError): """ Exception raised when a problem occurs at initialization time, in particular when checking the consistency of inputs `population` (see :meth:`initialize`). """ pass
[docs] class SimulationRunError(SimulationError): """ Exception raised when running a simulation, for example when a problem occurs during the integration of the system of differential equations. """ pass
[docs] class Simulation(object): """ The Simulation class allows to initialize and run the model. User should use method :meth:`initialize` to initialize the model, and method :meth:`run` to run the model. """ #: the name of the compartments attributes in the model MODEL_COMPARTMENTS_NAMES = {model.Plant: [], model.Axis: [], model.Roots: [], model.Phytomer: [], model.Organ: [], model.HiddenZone: ['leaf_L', 'turgor_water_potential', 'water_content', 'width', 'thickness'], model.PhotosyntheticOrganElement: ['length', 'turgor_water_potential', 'water_content', 'width', 'thickness'], model.Soil: ['water_content']} #: the time index T_INDEX = ['t'] # -------------------------------------------------------------------------- # DEFINITION OF THE PARAMETERS AND COMPUTED VARIABLES # -------------------------------------------------------------------------- # ---------- PLANT scale ---------- #: the index to locate the plants in the modelled system PLANTS_INDEXES = ['plant'] #: concatenation of :attr:`T_INDEX` and :attr:`PLANTS_INDEXES` PLANTS_T_INDEXES = T_INDEX + PLANTS_INDEXES #: the parameters which define the state of the modelled system at plant scale PLANTS_STATE_PARAMETERS = [] #: the variables which define the state of the modelled system at plant scale, #: formed be the concatenation of :attr:`PLANTS_STATE_PARAMETERS` and the names #: of the compartments associated to each plant (see :attr:`MODEL_COMPARTMENTS_NAMES`) PLANTS_STATE = PLANTS_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.Plant, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at plant scale PLANTS_INTERMEDIATE_VARIABLES = [] #: the fluxes exchanged between the compartments at plant scale PLANTS_FLUXES = [] #: the variables computed by integrating values of plant components parameters/variables recursively PLANTS_INTEGRATIVE_VARIABLES = [] #: all the variables computed during a run step of the simulation at plant scale PLANTS_RUN_VARIABLES = PLANTS_STATE + PLANTS_INTERMEDIATE_VARIABLES + PLANTS_FLUXES + PLANTS_INTEGRATIVE_VARIABLES # ---------- AXIS scale ---------- #: the indexes to locate the axes in the modelled system AXES_INDEXES = ['plant', 'axis'] #: concatenation of :attr:`T_INDEX` and :attr:`AXES_INDEXES` AXES_T_INDEXES = T_INDEX + AXES_INDEXES #: the parameters which define the state of the modelled system at axis scale AXES_STATE_PARAMETERS = ['SAM_temperature'] #: the variables which define the state of the modelled system at axis scale, #: formed be the concatenation of :attr:`AXES_STATE_PARAMETERS` and the names #: of the compartments associated to each axis (see :attr:`MODEL_COMPARTMENTS_NAMES`) AXES_STATE = AXES_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.Axis, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at axis scale AXES_INTERMEDIATE_VARIABLES = [] #: the fluxes exchanged between the compartments at axis scale AXES_FLUXES = [] #: the variables computed by integrating values of axis components parameters/variables recursively AXES_INTEGRATIVE_VARIABLES = ['Total_Transpiration_turgor', 'Growth', 'water_influx', 'plant_water_content'] #: all the variables computed during a run step of the simulation at axis scale AXES_RUN_VARIABLES = AXES_STATE + AXES_INTERMEDIATE_VARIABLES + AXES_FLUXES + AXES_INTEGRATIVE_VARIABLES # ---------- PHYTOMER scale ---------- #: the indexes to locate the phytomers in the modelled system PHYTOMERS_INDEXES = ['plant', 'axis', 'metamer'] #: concatenation of :attr:`T_INDEX` and :attr:`PHYTOMERS_INDEXES` PHYTOMERS_T_INDEXES = T_INDEX + PHYTOMERS_INDEXES #: the parameters which define the state of the modelled system at phytomer scale PHYTOMERS_STATE_PARAMETERS = [] #: the variables which define the state of the modelled system at phytomer scale, #: formed be the concatenation of :attr:`PHYTOMERS_STATE_PARAMETERS` and the names #: of the compartments associated to each phytomer (see :attr:`MODEL_COMPARTMENTS_NAMES`) PHYTOMERS_STATE = PHYTOMERS_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.Phytomer, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at phytomer scale PHYTOMERS_INTERMEDIATE_VARIABLES = [] #: the fluxes exchanged between the compartments at phytomer scale PHYTOMERS_FLUXES = [] #: the variables computed by integrating values of phytomer components parameters/variables recursively PHYTOMERS_INTEGRATIVE_VARIABLES = [] #: all the variables computed during a run step of the simulation at phytomer scale PHYTOMERS_RUN_VARIABLES = PHYTOMERS_STATE + PHYTOMERS_INTERMEDIATE_VARIABLES + PHYTOMERS_FLUXES + PHYTOMERS_INTEGRATIVE_VARIABLES # ---------- ORGAN scale ---------- #: the indexes to locate the organ in the modelled system ORGANS_INDEXES = ['plant', 'axis', 'organ'] #: concatenation of :attr:`T_INDEX` and :attr:`ORGAN_INDEXES` ORGANS_T_INDEXES = T_INDEX + ORGANS_INDEXES #: the parameters which define the state of the modelled system at organ scale ORGANS_STATE_PARAMETERS = ['age', 'amino_acids', 'proteins', 'sucrose', 'mstruct', 'Tr', 'green_area', 'SRWC', 'Tsoil'] #: the variables which define the state of the modelled system at organ scale, #: formed be the concatenation of :attr:`ORGAN_STATE_PARAMETERS` and the names #: of the compartments associated to each organ (see :attr:`MODEL_COMPARTMENTS_NAMES`) ORGANS_STATE = ORGANS_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.Organ, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at organ scale ORGANS_INTERMEDIATE_VARIABLES = ['water_potential'] #: the fluxes exchanged between the compartments at organ scale ORGANS_FLUXES = [] #: the variables computed by integrating values of xylem components parameters/variables recursively ORGANS_INTEGRATIVE_VARIABLES = [] #: all the variables computed during a run step of the simulation at organ scale ORGANS_RUN_VARIABLES = ORGANS_STATE + ORGANS_INTERMEDIATE_VARIABLES + ORGANS_FLUXES + ORGANS_INTEGRATIVE_VARIABLES # ---------- HIDDENZONE scale ---------- #: the indexes to locate the hidden zones in the modelled system HIDDENZONE_INDEXES = ['plant', 'axis', 'metamer'] #: concatenation of :attr:`T_INDEX` and :attr:`HIDDENZONE_INDEXES` HIDDENZONE_T_INDEXES = T_INDEX + HIDDENZONE_INDEXES #: the parameters which define the state of the modelled system at hidden zone scale HIDDENZONE_STATE_PARAMETERS = ['leaf_pseudo_age', 'leaf_pseudostem_length', 'fructan', 'amino_acids', 'proteins', 'sucrose', 'mstruct', 'hiddenzone_age', 'leaf_enclosed_mstruct', 'leaf_Wmax', 'length_hz_En', 'lamina_Lmax'] #: the variables which define the state of the modelled system at hidden zone scale, #: formed be the concatenation of :attr:`HIDDENZONE_STATE_PARAMETERS` and the names #: of the compartments associated to each hidden zone (see :attr:`MODEL_COMPARTMENTS_NAMES`) HIDDENZONE_STATE = HIDDENZONE_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.HiddenZone, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at hidden zone scale HIDDENZONE_INTERMEDIATE_VARIABLES = ['osmotic_water_potential', 'resistance', 'water_potential', 'volume', 'length', 'phi_width', 'phi_thickness', 'phi_length', 'phi_volume', 'epsilon_volume', 'organ_volume', 'WC_mstruct', 'omega', 'leaf_Lmax', 'delta_hiddenzone_dimensions_plastic', 'delta_weq', 'delta_leaf_L'] #: the fluxes exchanged between the compartments at hidden zone scale HIDDENZONE_FLUXES = ['water_influx', 'water_outflow', 'Growth'] #: the variables computed by integrating values of hidden zone components parameters/variables recursively HIDDENZONE_INTEGRATIVE_VARIABLES = [] #: all the variables computed during a run step of the simulation at plant scale HIDDENZONE_RUN_VARIABLES = HIDDENZONE_STATE + HIDDENZONE_INTERMEDIATE_VARIABLES + HIDDENZONE_FLUXES + HIDDENZONE_INTEGRATIVE_VARIABLES # ---------- ELEMENT scale ---------- #: the indexes to locate the ELEMENTS in the modelled system ELEMENTS_INDEXES = ['plant', 'axis', 'metamer', 'organ', 'element'] #: concatenation of :attr:`T_INDEX` and :attr:`ELEMENTS_INDEXES` ELEMENTS_T_INDEXES = T_INDEX + ELEMENTS_INDEXES #: the parameters which define the state of the modelled system at organ scale ELEMENTS_STATE_PARAMETERS = ['amino_acids', 'green_area', 'mstruct', 'proteins', 'sucrose', 'fructan', 'Ts', 'Tr', 'age', 'is_growing', 'Wmax'] #: the variables which define the state of the modelled system at organ scale, #: formed be the concatenation of :attr:`ELEMENTS_STATE_PARAMETERS` and the names #: of the compartments associated to each organ (see :attr:`MODEL_COMPARTMENTS_NAMES`) ELEMENTS_STATE = ELEMENTS_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.PhotosyntheticOrganElement, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at organ scale ELEMENTS_INTERMEDIATE_VARIABLES = ['osmotic_water_potential', 'resistance', 'water_potential', 'volume', 'epsilon_volume', 'organ_volume', 'WC_mstruct'] #: the fluxes exchanged between the compartments at organ scale ELEMENTS_FLUXES = ['water_influx'] #: the variables computed by integrating values of organ components parameters/variables recursively ELEMENTS_INTEGRATIVE_VARIABLES = ['Total_Transpiration_turgor', 'total_water_influx'] #: all the variables computed during a run step of the simulation at organ scale ELEMENTS_RUN_VARIABLES = ELEMENTS_STATE + ELEMENTS_INTERMEDIATE_VARIABLES + ELEMENTS_FLUXES + ELEMENTS_INTEGRATIVE_VARIABLES # ---------- SOIL scale ---------- #: the indexes to locate the soils in the modelled system SOILS_INDEXES = ['plant', 'axis'] #: concatenation of :attr:`T_INDEX` and :attr:`SOILS_INDEXES` SOILS_T_INDEXES = T_INDEX + SOILS_INDEXES #: the parameters which define the state of the modelled system at soil scale SOILS_STATE_PARAMETERS = [] #: the variables which define the state of the modelled system at soil scale, #: formed be the concatenation of :attr:`SOILS_STATE_PARAMETERS` and the names #: of the compartments associated to each soil (see :attr:`MODEL_COMPARTMENTS_NAMES`) SOILS_STATE = SOILS_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.Soil, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at soil scale SOILS_INTERMEDIATE_VARIABLES = ['SRWC', 'water_potential'] #: the fluxes exchanged between the compartments at soil scale SOILS_FLUXES = [] #: the variables computed by integrating values of soil components parameters/variables recursively SOILS_INTEGRATIVE_VARIABLES = [] #: all the variables computed during a run step of the simulation at soil scale SOILS_RUN_VARIABLES = SOILS_STATE + SOILS_INTERMEDIATE_VARIABLES + SOILS_FLUXES + SOILS_INTEGRATIVE_VARIABLES #: a dictionary of all the variables which define the state of the modelled system, for each scale ALL_STATE_PARAMETERS = {model.Plant: PLANTS_STATE_PARAMETERS, model.Axis: AXES_STATE_PARAMETERS, model.Organ: ORGANS_STATE_PARAMETERS, model.Phytomer: PHYTOMERS_STATE_PARAMETERS, model.HiddenZone: HIDDENZONE_STATE_PARAMETERS, model.PhotosyntheticOrganElement: ELEMENTS_STATE_PARAMETERS, model.Soil: SOILS_STATE_PARAMETERS} #: the names of the elements forcing ELEMENTS_FORCING = ('green_area', 'Tr') #: the name of the loggers for compartments and derivatives LOGGERS_NAMES = {'compartments': {model.Plant: 'hydraulics.compartments.plants', model.Axis: 'hydraulics.compartments.axes', model.Phytomer: 'hydraulics.compartments.phytomers', model.Organ: 'hydraulics.compartments.organs', model.HiddenZone: 'hydraulics.compartments.hiddenzones', model.PhotosyntheticOrganElement: 'hydraulics.compartments.elements'}, 'derivatives': {model.Plant: 'hydraulics.derivatives.plants', model.Axis: 'hydraulics.derivatives.axes', model.Phytomer: 'hydraulics.derivatives.phytomers', model.Organ: 'hydraulics.derivatives.organs', model.HiddenZone: 'hydraulics.derivatives.hiddenzones', model.PhotosyntheticOrganElement: 'hydraulics.derivatives.elements'}} def __init__(self, delta_t=1, interpolate_forcing=False, elements_forcing_delta_t=None, hiddenzone_forcing_delta_t=None): self.population = model.Population() #: the population to simulate on #: The inputs of the soils. #: #: `soils` is a dictionary of objects of type :class:`model.Soil`: #: {(plant_index, axis_label): soil_object, ...} self.soils = {} self.initial_conditions = [] #: the initial conditions of the compartments in the population and soil self.initial_conditions_mapping = {} #: dictionary to map the compartments to their indexes in :attr:`initial_conditions` self.delta_t = delta_t #: the delta t of the simulation (in seconds) self.time_step = self.delta_t / 3600.0 #: time step of the simulation (in hours) self.time_grid = np.array([0.0, self.time_step]) #: the time grid of the simulation (in hours) self.interpolate_forcing = interpolate_forcing #: a boolean flag which indicates if we want to interpolate or not the forcing (True: interpolate, False: do not interpolate) # set the loggers for compartments and derivatives compartments_logger = logging.getLogger('hydraulics.compartments') derivatives_logger = logging.getLogger('hydraulics.derivatives') if compartments_logger.isEnabledFor(logging.DEBUG) or derivatives_logger.isEnabledFor(logging.DEBUG): sep = ',' if compartments_logger.isEnabledFor(logging.DEBUG): plants_compartments_logger = logging.getLogger('hydraulics.compartments.plants') plants_compartments_logger.debug(sep.join(Simulation.PLANTS_T_INDEXES + Simulation.PLANTS_STATE)) axes_compartments_logger = logging.getLogger('hydraulics.compartments.axes') axes_compartments_logger.debug(sep.join(Simulation.AXES_T_INDEXES + Simulation.AXES_STATE)) phytomers_compartments_logger = logging.getLogger('hydraulics.compartments.phytomers') phytomers_compartments_logger.debug(sep.join(Simulation.PHYTOMERS_T_INDEXES + Simulation.PHYTOMERS_STATE)) organs_compartments_logger = logging.getLogger('hydraulics.compartments.organs') organs_compartments_logger.debug(sep.join(Simulation.ORGANS_T_INDEXES + Simulation.ORGANS_STATE)) hiddenzones_compartments_logger = logging.getLogger('hydraulics.compartments.hiddenzones') hiddenzones_compartments_logger.debug(sep.join(Simulation.HIDDENZONE_T_INDEXES + Simulation.HIDDENZONE_STATE + Simulation.HIDDENZONE_INTERMEDIATE_VARIABLES)) elements_compartments_logger = logging.getLogger('hydraulics.compartments.elements') elements_compartments_logger.debug(sep.join(Simulation.ELEMENTS_T_INDEXES + Simulation.ELEMENTS_STATE + Simulation.ELEMENTS_INTERMEDIATE_VARIABLES)) organs_compartments_logger = logging.getLogger('hydraulics.compartments.organs') organs_compartments_logger.debug(sep.join(Simulation.ORGANS_T_INDEXES + Simulation.ORGANS_STATE)) soils_compartments_logger = logging.getLogger('hydraulics.compartments.soils') soils_compartments_logger.debug(sep.join(Simulation.SOILS_T_INDEXES + Simulation.SOILS_STATE)) if derivatives_logger.isEnabledFor(logging.DEBUG): plants_derivatives_logger = logging.getLogger('hydraulics.derivatives.plants') plants_derivatives_logger.debug(sep.join(Simulation.PLANTS_T_INDEXES + Simulation.PLANTS_STATE)) axes_derivatives_logger = logging.getLogger('hydraulics.derivatives.axes') axes_derivatives_logger.debug(sep.join(Simulation.AXES_T_INDEXES + Simulation.AXES_STATE)) phytomers_derivatives_logger = logging.getLogger('hydraulics.derivatives.phytomers') phytomers_derivatives_logger.debug(sep.join(Simulation.PHYTOMERS_T_INDEXES + Simulation.PHYTOMERS_STATE)) organs_derivatives_logger = logging.getLogger('hydraulics.derivatives.organs') organs_derivatives_logger.debug(sep.join(Simulation.ORGANS_T_INDEXES + Simulation.ORGANS_STATE)) hiddenzones_derivatives_logger = logging.getLogger('hydraulics.derivatives.hiddenzones') hiddenzones_derivatives_logger.debug(sep.join(Simulation.HIDDENZONE_T_INDEXES + Simulation.HIDDENZONE_STATE + Simulation.HIDDENZONE_INTERMEDIATE_VARIABLES)) elements_derivatives_logger = logging.getLogger('hydraulics.derivatives.elements') elements_derivatives_logger.debug(sep.join(Simulation.ELEMENTS_T_INDEXES + Simulation.ELEMENTS_STATE + Simulation.ELEMENTS_INTERMEDIATE_VARIABLES)) organs_derivatives_logger = logging.getLogger('hydraulics.derivatives.organs') organs_derivatives_logger.debug(sep.join(Simulation.ORGANS_T_INDEXES + Simulation.ORGANS_STATE)) soils_derivatives_logger = logging.getLogger('hydraulics.derivatives.soils') soils_derivatives_logger.debug(sep.join(Simulation.SOILS_T_INDEXES + Simulation.SOILS_STATE)) logger = logging.getLogger(__name__) if logger.isEnabledFor(logging.DEBUG): self.t_offset = 0.0 #: the absolute time offset elapsed from the beginning of the simulation if interpolate_forcing: if (elements_forcing_delta_t is not None and hiddenzone_forcing_delta_t is not None): self.elements_forcing_delta_t_ratio = elements_forcing_delta_t / delta_t #: the ratio between the delta t of the elements forcing and the delta t of the simulation self.hiddenzone_forcing_delta_t_ratio = hiddenzone_forcing_delta_t / delta_t #: the ratio between the delta t of the hiddenzone forcing and the delta t of the simulation elif elements_forcing_delta_t is None: message = """The value of `interpolate_forcing` passed to the Simulation constructor is `True`, but `elements_forcing_delta_t` is `None`. Please set `elements_forcing_delta_t` (through the Simulation constructor) to a not `None` value.""" logger.exception(message) raise SimulationConstructionError(message) elif hiddenzone_forcing_delta_t is None: message = """The value of `interpolate_forcing` passed to the Simulation constructor is `True`, but `hiddenzone_forcing_delta_t` is `None`. Please set `hiddenzone_forcing_delta_t` (through the Simulation constructor) to a not `None` value.""" logger.exception(message) raise SimulationConstructionError(message) elif elements_forcing_delta_t < delta_t: message = """The value of `elements_forcing_delta_t` passed to the Simulation constructor is lesser than the one of `delta_t`. Please set a `elements_forcing_delta_t` that is at least equal to `delta_t`.""" logger.exception(message) raise SimulationConstructionError(message) elif hiddenzone_forcing_delta_t < delta_t: message = """The value of `hiddenzone_forcing_delta_t` passed to the Simulation constructor is lesser than the one of `delta_t`. Please set a `hiddenzone_forcing_delta_t` that is at least equal to `delta_t`.""" logger.exception(message) raise SimulationConstructionError(message) self.previous_forcing_values = {} #: previous values of the forcing self.new_forcing_values = {} #: new values of the forcing self.interpolation_functions = {} #: functions to interpolate the forcing self.nfev_total = 0 #: cumulative number of RHS function evaluations
[docs] def initialize(self, population, soils): """ Initialize: * :attr:`population`, * :attr:`soils`, * :attr:`initial_conditions_mapping`, * and :attr:`initial_conditions` from `population` and `soils`. :param model.Population population: a population of plants. :param dict soils: the soil associated to each axis. `soils` must be a dictionary with the same structure as :attr:`soils` """ logger = logging.getLogger(__name__) logger.info('Initialization of the simulation...') # clean the attributes of the simulation del self.population.plants[:] self.soils.clear() del self.initial_conditions[:] self.initial_conditions_mapping.clear() # create new population and soil self.population.plants.extend(population.plants) self.soils.update(soils) # check the consistency of population if len(self.population.plants) != 0: # population must contain at least 1 plant for plant in self.population.plants: if len(plant.axes) != 0: # each plant must contain at least 1 axis for axis in plant.axes: if axis.roots is None: # each axis must have a "roots" message = 'No roots found in (plant={},axis={})'.format(plant.index, axis.label) logger.exception(message) raise SimulationInitializationError(message) if axis.xylem is None: # each axis must have a xylem message = 'No xylem found in (plant={},axis={})'.format(plant.index, axis.label) logger.exception(message) raise SimulationInitializationError(message) if len(axis.phytomers) != 0: # each axis must contain at least 1 phytomer for phytomer in axis.phytomers: # phytomer_organs = (phytomer.lamina, phytomer.internode, phytomer.sheath) phytomer_organs = (phytomer.lamina, phytomer.sheath) # each phytomer must contain at least 1 photosynthetic organ or an hidden zone if phytomer_organs.count(None) != len(phytomer_organs) or phytomer.hiddenzone is not None: for organ in phytomer_organs: if organ is not None: organ_elements = (organ.exposed_element, organ.enclosed_element) # each photosynthetic organ must contain at least 1 element if organ_elements.count(None) != len(organ_elements): for element in organ_elements: if element is not None: # an element must belong to an organ of the same type (e.g. a LaminaElement must belong to a Lamina) if organ.__class__.__name__ not in element.__class__.__name__: message = 'In (plant={},axis={},phytomer={}), a {} belongs to a {}'.format(plant.index, axis.label, phytomer.index, element.__class__.__name__, organ.__class__.__name__) logger.exception(message) raise SimulationInitializationError(message) else: message = 'No element found in (plant={},axis={},phytomer={},organ={})'.format(plant.index, axis.label, phytomer.index, organ.label) logger.exception(message) raise SimulationInitializationError(message) else: message = 'Neither photosynthetic organ nor hidden growing zone found in (plant={},axis={},phytomer={})'.format(plant.index, axis.label, phytomer.index) logger.exception(message) raise SimulationInitializationError(message) else: message = 'No phytomer found in (plant={},axis={})'.format(plant.index, axis.label) logger.exception(message) raise SimulationInitializationError(message) if (plant.index, axis.label) not in self.soils: # each axis must be associated to a soil message = 'No soil found in (plant={},axis={})'.format(plant.index, axis.label) logger.exception(message) raise SimulationInitializationError(message) else: message = 'No axis found in (plant={})'.format(plant.index) logger.exception(message) raise SimulationInitializationError(message) else: message = 'No plant found in the population.' logger.exception(message) raise SimulationInitializationError(message) if self.interpolate_forcing: # Save the new value of each forcing and set the state parameters to the previous forcing values. self.new_forcing_values.clear() for plant in self.population.plants: for axis in plant.axes: for organ in (axis.xylem, axis.roots): xylem_id = (plant.index, axis.label, organ.label) self.new_forcing_values[xylem_id] = {} forcing_labels = {} self.new_forcing_values[xylem_id][forcing_labels] = getattr(axis.xylem, forcing_labels) if axis.xylem in self.previous_forcing_values: setattr(axis.xylem, forcing_labels, self.previous_forcing_values[xylem_id][forcing_labels]) for phytomer in axis.phytomers: for organ in (phytomer.lamina, phytomer.sheath): if organ is None: continue for element in (organ.exposed_element, organ.enclosed_element): if element is not None: element_id = (plant.index, axis.label, phytomer.index, organ.label, element.label) self.new_forcing_values[element_id] = {} for forcing_label in Simulation.ELEMENTS_FORCING: self.new_forcing_values[element_id][forcing_label] = getattr(element, forcing_label) if element_id in self.previous_forcing_values: setattr(element, forcing_label, self.previous_forcing_values[element_id][forcing_label]) # initialize initial conditions def _init_initial_conditions(model_object, i): class_ = model_object.__class__ if issubclass(class_, model.HiddenZone): class_ = model.HiddenZone elif issubclass(class_, model.Organ): class_ = model.Organ elif issubclass(class_, model.PhotosyntheticOrganElement): class_ = model.PhotosyntheticOrganElement compartments_names = Simulation.MODEL_COMPARTMENTS_NAMES[class_] self.initial_conditions_mapping[model_object] = {} for compartment_name in compartments_names: if hasattr(model_object, compartment_name): self.initial_conditions_mapping[model_object][compartment_name] = i self.initial_conditions.append(0) i += 1 return i i = 0 for soil in soils.values(): i = _init_initial_conditions(soil, i) for plant in self.population.plants: i = _init_initial_conditions(plant, i) for axis in plant.axes: i = _init_initial_conditions(axis, i) for organ in (axis.roots, axis.xylem): if organ is None: continue i = _init_initial_conditions(organ, i) for phytomer in axis.phytomers: i = _init_initial_conditions(phytomer, i) if phytomer.hiddenzone: i = _init_initial_conditions(phytomer.hiddenzone, i) # for organ in (phytomer.lamina, phytomer.internode, phytomer.sheath): for organ in (phytomer.lamina, phytomer.sheath): if organ: for element in (organ.exposed_element, organ.enclosed_element): if element: i = _init_initial_conditions(element, i) self.population.calculate_aggregated_variables() logger.info('Initialization of the simulation DONE')
[docs] def run(self): """ Compute turgor pressure driven growth in :attr:`population` over :attr:`delta_t`. """ logger = logging.getLogger(__name__) logger.info('Run of Hydraulics...') if self.interpolate_forcing: # interpolate the forcing self._interpolate_forcing() self._update_initial_conditions() if logger.isEnabledFor(logging.DEBUG): logger.debug("Run the solver with delta_t = %s", self.time_step) #: Call :func:`scipy.integrate.solve_ivp` to integrate the system over self.time_grid. sol = solve_ivp(fun=self._calculate_all_derivatives, t_span=self.time_grid, y0=self.initial_conditions, method='LSODA', t_eval=np.array([self.time_step]), dense_output=False) self.nfev_total += sol.nfev if logger.isEnabledFor(logging.DEBUG): logger.debug("Run of the solver DONE") # check the integration ; raise an exception if the integration failed if not sol.success: message = "Integration failed: {}".format(sol.message) logger.exception(message) raise SimulationRunError(message) # Re-compute integrative variables self.population.calculate_aggregated_variables() if logger.isEnabledFor(logging.DEBUG): self.t_offset += self.time_step logger.info('Run of Hydraulics DONE')
def _update_initial_conditions(self): """Update the compartments values in :attr:`initial_conditions` from the compartments values of :attr:`population`. """ for model_object, compartments in self.initial_conditions_mapping.items(): for compartment_name, compartment_index in compartments.items(): self.initial_conditions[compartment_index] = getattr(model_object, compartment_name) def _interpolate_forcing(self): """Create functions to interpolate the forcing of the model to any time inside the time grid (see `self.time_grid`). If this is the first run of the model, then we consider that the forcing are constant. The interpolation functions are stored in :attr:`interpolation_functions`, and will be used later on and as needed by the SciPy solver. """ self.interpolation_functions.clear() next_forcing_values = {} for plant in self.population.plants: for axis in plant.axes: for phytomer in axis.phytomers: for organ in (phytomer.lamina, phytomer.sheath): if organ is None: continue for element in (organ.exposed_element, organ.enclosed_element): if element is not None: element_id = (plant.index, axis.label, phytomer.index, organ.label, element.label) self.interpolation_functions[element_id] = {} next_forcing_values[element_id] = {} forcing_labels = Simulation.ELEMENTS_FORCING forcing_delta_t_ratio = self.elements_forcing_delta_t_ratio for forcing_label in forcing_labels: if element_id in self.previous_forcing_values and \ self.previous_forcing_values[element_id][forcing_label] != self.new_forcing_values[element_id][forcing_label]: prev_forcing_value = self.previous_forcing_values[element_id][forcing_label] next_forcing_value = prev_forcing_value + (self.new_forcing_values[element_id][forcing_label] - prev_forcing_value) / forcing_delta_t_ratio else: next_forcing_value = self.new_forcing_values[element_id][forcing_label] prev_forcing_value = next_forcing_value self.interpolation_functions[element_id][forcing_label] = interpolate.interp1d(self.time_grid, [prev_forcing_value, next_forcing_value], assume_sorted=True) next_forcing_values[element_id][forcing_label] = next_forcing_value self.previous_forcing_values.clear() self.previous_forcing_values.update(next_forcing_values) def _log_compartments(self, t, y, loggers_names): """Log the values in `y` to the loggers in `loggers_names`. """ def update_rows(model_object, indexes, rows, i): """Update list `rows` appending a new row corresponding to the compartment values associated to object `model_object` located at indexes `indexes`. `i` is used to reach the values associated to object `model_object` from array `y`. """ row = [] class_ = model_object.__class__ intermediate_variables = None if issubclass(class_, model.HiddenZone): class_ = model.HiddenZone intermediate_variables = Simulation.HIDDENZONE_INTERMEDIATE_VARIABLES elif issubclass(class_, model.PhotosyntheticOrganElement): class_ = model.PhotosyntheticOrganElement intermediate_variables = Simulation.ELEMENTS_INTERMEDIATE_VARIABLES elif issubclass(class_, model.Organ): class_ = model.Organ intermediate_variables = Simulation.ORGANS_INTERMEDIATE_VARIABLES parameters_names = Simulation.ALL_STATE_PARAMETERS[class_] + intermediate_variables for parameter_name in parameters_names: if hasattr(model_object, parameter_name): row.append(str(getattr(model_object, parameter_name))) else: row.append('NA') compartments_names = Simulation.MODEL_COMPARTMENTS_NAMES[class_] for compartment_name in compartments_names: if hasattr(model_object, compartment_name): row.append(str(y[i])) i += 1 else: row.append('NA') rows.append([str(index) for index in indexes] + row) return i i = 0 all_rows = dict([(class_, []) for class_ in loggers_names]) for soil_id, soil in self.soils.items(): i = update_rows(soil, (t,) + soil_id, all_rows[model.Soil], i) for plant in self.population.plants: for axis in plant.axes: for organ in (axis.roots, axis.xylem): if organ is None: continue i = update_rows(organ, [t, plant.index, axis.label, organ.label], all_rows[model.Organ], i) for phytomer in axis.phytomers: # for organ in (phytomer.lamina, phytomer.internode, phytomer.sheath, phytomer.hiddenzone): for organ in (phytomer.lamina, phytomer.sheath, phytomer.hiddenzone): if organ is None: continue if organ is phytomer.hiddenzone: i = update_rows(organ, [t, plant.index, axis.label, phytomer.index], all_rows[ model.HiddenZone], i) continue for element in (organ.exposed_element, organ.enclosed_element): if element is None: continue i = update_rows(element, [t, plant.index, axis.label, phytomer.index, organ.label, element.label], all_rows[ model.PhotosyntheticOrganElement], i) row_sep = '\n' column_sep = ',' for class_, logger_name in loggers_names.items(): compartments_logger = logging.getLogger(logger_name) formatted_initial_conditions = row_sep.join([column_sep.join(row) for row in all_rows[class_]]) compartments_logger.debug(formatted_initial_conditions) def _calculate_all_derivatives(self, t, y): """Compute the derivative of `y` at `t`. :meth:`_calculate_all_derivatives` is passed as **func** argument to :func:`solve_ivp(fun, t_span, y0,...) <scipy.integrate.solve_ivp>`. :meth:`_calculate_all_derivatives` is called automatically by :func:`scipy.integrate.solve_ivp <scipy.integrate.solve_ivp>`. First call to :meth:`_calculate_all_derivatives` uses `y` = **y0** and `t` = **t_span** [0], where **y0** and **t_span** are arguments passed to :func:`solve_ivp(fun, t_span, y0,...) <scipy.integrate.solve_ivp>`. Following calls to :meth:`_calculate_all_derivatives` use `t` in [**t_span** [0], **t_span** [1]]. :param float t: The current t at which we want to compute the derivatives. Values of `t` are chosen automatically by :func:`scipy.integrate.solve_ivp`. At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp` `t` = **t_span** [0], where **t_span** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) <scipy.integrate.solve_ivp>`. For each following call to :meth:`_calculate_all_derivatives`, `t` belongs to the interval [**t_span** [0], **t_span** [1]]. :param list y: The current values of y. At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp`, `y` = **y0** where **y0** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) <scipy.integrate.solve_ivp>`. Then, values of `y` are chosen automatically by :func:`scipy.integrate.solve_ivp`. :return: The derivatives of `y` at `t`. :rtype: list """ logger = logging.getLogger(__name__) if logger.isEnabledFor(logging.DEBUG): t_abs = t + self.t_offset logger.debug('t = {}'.format(t_abs)) if self.interpolate_forcing: # Update state parameters using interpolation functions for plant in self.population.plants: for axis in plant.axes: for phytomer in axis.phytomers: for organ in (phytomer.lamina, phytomer.sheath): if organ is None: continue for element in (organ.exposed_element, organ.enclosed_element): if element is not None: element_id = (plant.index, axis.label, phytomer.index, organ.label, element.label) for forcing_label in Simulation.ELEMENTS_FORCING: setattr(element, forcing_label, float(self.interpolation_functions[element_id][forcing_label](t))) # Compute integrative variables self.population.calculate_aggregated_variables() compartments_logger = logging.getLogger('hydraulics.compartments') if logger.isEnabledFor(logging.DEBUG) and compartments_logger.isEnabledFor(logging.DEBUG): self._log_compartments(t_abs, y, Simulation.LOGGERS_NAMES['compartments']) # Check that the solver is not crashed y_isnan = np.isnan(y) if y_isnan.any(): message = 'The solver did not manage to compute a compartment. See the logs. NaN found in y' logger.exception(message) raise SimulationRunError(message) y_derivatives = np.zeros_like(y) # TODO: TEMP !!!! soil_water_outputs = 0 soil = self.soils[(1, 'MS')] soil.water_content = y[self.initial_conditions_mapping[soil]['water_content']] soil.SRWC = soil.calculate_SRWC(soil.water_content) soil.water_potential = soil.calculate_water_potential(soil.SRWC) #: Water flux with xylem and organs for plant in self.population.plants: for axis in plant.axes: # Xylem #: Total water potential axis.xylem.water_potential = axis.xylem.calculate_xylem_water_potential(soil.water_potential, axis.water_influx, axis.Growth, self.delta_t) for phytomer in axis.phytomers: # Hidden zone hiddenzone = phytomer.hiddenzone if hiddenzone is not None: hiddenzone.leaf_L = y[self.initial_conditions_mapping[hiddenzone]['leaf_L']] hiddenzone.thickness = y[self.initial_conditions_mapping[hiddenzone]['thickness']] hiddenzone.width = y[self.initial_conditions_mapping[hiddenzone]['width']] # Update of leaf_Lmax hiddenzone.leaf_Lmax = hiddenzone.leaf_L if hiddenzone.leaf_pseudo_age == 0: #: First time after previous leaf emergence #: Width and thickness thickness_ratio = parameters.HIDDEN_ZONE_PARAMETERS.TL_ratio width_ratio = parameters.HIDDEN_ZONE_PARAMETERS.WL_ratio hiddenzone.width = hiddenzone.leaf_L * width_ratio hiddenzone.thickness = hiddenzone.leaf_L * thickness_ratio hiddenzone.leaf_Wmax = hiddenzone.width #: Volume & water content as function of dimensions hiddenzone.volume = hiddenzone.length * hiddenzone.width * hiddenzone.thickness hiddenzone.water_content = hiddenzone.volume * parameters.RHO_WATER #: Osmotic water potential hiddenzone.osmotic_water_potential = hiddenzone.calculate_osmotic_water_potential(hiddenzone.fructan, hiddenzone.sucrose, hiddenzone.amino_acids, hiddenzone.volume, axis.SAM_temperature) #: Total water potential hiddenzone.water_potential = axis.xylem.water_potential #: Turgor water potential hiddenzone.turgor_water_potential = hiddenzone.water_potential - hiddenzone.osmotic_water_potential #: Length hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) elif hiddenzone.leaf_pseudo_age > 0: #: After previous leaf emergence #: Turgor water potential hiddenzone.turgor_water_potential = y[self.initial_conditions_mapping[hiddenzone]['turgor_water_potential']] #: Volume and water content hiddenzone.water_content = y[self.initial_conditions_mapping[hiddenzone]['water_content']] hiddenzone.volume = hiddenzone.calculate_volume(hiddenzone.water_content) #: Osmotic water potential hiddenzone.osmotic_water_potential = hiddenzone.calculate_osmotic_water_potential(hiddenzone.fructan, hiddenzone.sucrose, hiddenzone.amino_acids, hiddenzone.volume, axis.SAM_temperature) #: Total water potential hiddenzone.water_potential = hiddenzone.calculate_water_potential(hiddenzone.turgor_water_potential, hiddenzone.osmotic_water_potential) #: Length hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) else: #: Before previous leaf emergence (calculation in morphogenesis) continue #: Resistance to water flow hiddenzone_dimensions = {'length': hiddenzone.length, 'thickness': hiddenzone.thickness, 'width': hiddenzone.width} hiddenzone.resistance = hiddenzone.calculate_resistance(hiddenzone_dimensions) #: Flows with xylem hiddenzone.water_influx = hiddenzone.calculate_water_flux(hiddenzone.water_potential, axis.xylem.water_potential, hiddenzone.resistance, self.delta_t) hiddenzone.water_outflow = 0 #: No water flow between hiddenzone and element # Photosynthetic Organ Elements # for organ in (phytomer.lamina, phytomer.internode, phytomer.sheath): for organ in (phytomer.lamina, phytomer.sheath): if organ is None: continue for element in (organ.exposed_element, organ.enclosed_element): if element is None: continue element.length = y[self.initial_conditions_mapping[element]['length']] element.width = y[self.initial_conditions_mapping[element]['width']] element.thickness = y[self.initial_conditions_mapping[element]['thickness']] element.organ_dimensions = {'length': element.length, 'width': element.width, 'thickness': element.thickness} if element.age == 0: #: First time after element emergence #: Width and thickness of the hiddenzone if organ.label == "blade": element.thickness = hiddenzone.thickness element.width = hiddenzone.width #: Volume and water content as function of dimensions element.volume = element.calculate_organ_volume(element.organ_dimensions) element.water_content = element.volume * parameters.RHO_WATER # : Osmotic water potential element.osmotic_water_potential = element.calculate_osmotic_water_potential(element.sucrose, element.amino_acids, element.volume, element.temperature, element.fructan) #: Total water potential element.water_potential = axis.xylem.water_potential # : Turgor water potential element.turgor_water_potential = element.water_potential - element.osmotic_water_potential # Length of the HZ if hiddenzone is not None: if organ.label == "blade": hiddenzone.length = hiddenzone.length_hz_En elif element.age > 0: #: Emerged element #: Water content element.water_content = y[self.initial_conditions_mapping[element]['water_content']] # : Volume element.volume = element.calculate_volume(element.water_content) # : Turgor water potential element.turgor_water_potential = y[self.initial_conditions_mapping[element]['turgor_water_potential']] #: Osmotic water potential element.osmotic_water_potential = element.calculate_osmotic_water_potential(element.sucrose, element.amino_acids, element.volume, element.temperature, element.fructan) #: Total water potential element.water_potential = element.calculate_water_potential(element.turgor_water_potential, element.osmotic_water_potential) # # Length of the HZ if hiddenzone is not None: if organ.label == "blade": hiddenzone.length = hiddenzone.length_hz_En #: Resistance to water flow element.resistance = element.calculate_resistance(element.organ_dimensions) #: Water fluxes with xylem element.water_influx = element.calculate_water_flux(element.water_potential, axis.xylem.water_potential, element.resistance, self.delta_t) #: compute the derivative of each compartment of element for plant in self.population.plants: for axis in plant.axes: for phytomer in axis.phytomers: # Hidden zone hiddenzone = phytomer.hiddenzone if hiddenzone is not None: #: Delta water content delta_water_content_hz = hiddenzone.calculate_delta_water_content(hiddenzone.water_influx, hiddenzone.water_outflow) #: Extensibility hiddenzone_delta_teq = hiddenzone.calculate_time_equivalent_Tref(axis.SAM_temperature, self.delta_t) phi = hiddenzone.calculate_extensibility_temperature(hiddenzone.leaf_pseudo_age, hiddenzone_delta_teq, self.delta_t) hiddenzone.phi_length = phi['z'] # extensibility for length hiddenzone.phi_width = phi['x'] # extensibility for length hiddenzone.phi_thickness = phi['y'] # extensibility for length hiddenzone.phi_volume = phi['x'] + phi['y'] + phi['z'] # volumetric extensibility #: Elasticity epsilon_x, epsilon_y, epsilon_z = hiddenzone.PARAMETERS.epsilon['x'], hiddenzone.PARAMETERS.epsilon['y'], hiddenzone.PARAMETERS.epsilon['z'] hiddenzone.epsilon_volume = (epsilon_x * epsilon_y * epsilon_z) / (epsilon_z * epsilon_x + epsilon_z * epsilon_y + epsilon_x * epsilon_y) #: Elastic reversible growth (MPa) if hiddenzone.leaf_pseudo_age > 0: #: After previous leaf emergence #: Derivatives #: Delta turgor pressure delta_turgor_water_potential = hiddenzone.calculate_delta_turgor_water_potential(phi, hiddenzone.turgor_water_potential, hiddenzone.volume, delta_water_content_hz) #: Dimensions with plastic and elastic deformation hiddenzone_dimensions = {'length': hiddenzone.length, 'width': hiddenzone.width, 'thickness': hiddenzone.thickness} delta_hiddenzone_dimensions_plastic = hiddenzone.calculate_delta_organ_dimensions_plastic(hiddenzone.turgor_water_potential, phi, hiddenzone_dimensions) delta_hiddenzone_dimensions_elastic = hiddenzone.calculate_delta_organ_dimensions_elastic(delta_turgor_water_potential, hiddenzone_dimensions) # Saving into outputs hiddenzone.delta_hiddenzone_dimensions_plastic = delta_hiddenzone_dimensions_plastic['length'] * 1000 if hiddenzone.leaf_L >= hiddenzone.leaf_pseudostem_length: # Emerged blade # Growing leaf with emerged blade y_derivatives[self.initial_conditions_mapping[hiddenzone]['water_content']] = 0 # Transfer of water content to emerged element y_derivatives[self.initial_conditions_mapping[hiddenzone]['turgor_water_potential']] = delta_turgor_water_potential # Elastic deformation y_derivatives[self.initial_conditions_mapping[hiddenzone]['width']] = delta_hiddenzone_dimensions_elastic['width'] y_derivatives[self.initial_conditions_mapping[hiddenzone]['thickness']] = delta_hiddenzone_dimensions_elastic['thickness'] y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] = delta_hiddenzone_dimensions_elastic['length'] else: # Enclosed blade y_derivatives[self.initial_conditions_mapping[hiddenzone]['water_content']] = delta_water_content_hz y_derivatives[self.initial_conditions_mapping[hiddenzone]['turgor_water_potential']] = delta_turgor_water_potential # Plastic deformation y_derivatives[self.initial_conditions_mapping[hiddenzone]['thickness']] = delta_hiddenzone_dimensions_elastic['thickness'] + delta_hiddenzone_dimensions_plastic['thickness'] y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] = delta_hiddenzone_dimensions_elastic['length'] + delta_hiddenzone_dimensions_plastic['length'] y_derivatives[self.initial_conditions_mapping[hiddenzone]['width']] = delta_hiddenzone_dimensions_elastic['width'] + delta_hiddenzone_dimensions_plastic['width'] hiddenzone.delta_leaf_L = y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] hiddenzone.leaf_Wmax += delta_hiddenzone_dimensions_plastic['width'] hiddenzone.organ_volume = hiddenzone.calculate_organ_volume(hiddenzone_dimensions) hiddenzone.WC_mstruct = hiddenzone.water_content / (hiddenzone.water_content + hiddenzone.mstruct) * 100 else: #: Before previous leaf emergence continue # Photosynthetic Organ Elements # for organ in (phytomer.lamina, phytomer.internode, phytomer.sheath): for organ in (phytomer.lamina, phytomer.sheath): if organ is None: continue for element in (organ.exposed_element, organ.enclosed_element): if element is None: continue epsilon_x, epsilon_y, epsilon_z = element.PARAMETERS.epsilon['x'], element.PARAMETERS.epsilon['y'], element.PARAMETERS.epsilon['z'] element.epsilon_volume = (epsilon_z * epsilon_x * epsilon_y) / (epsilon_z * epsilon_x + epsilon_z * epsilon_y + epsilon_x * epsilon_y) #: Elastic reversible growth (MPa) element.organ_dimensions = {'length': element.length, 'width': element.width, 'thickness': element.thickness} #: Delta water content delta_water_content_ele = element.calculate_delta_water_content(element.water_influx, element.Total_Transpiration_turgor) #: Delta turgor pressure delta_turgor_water_potential = element.calculate_delta_turgor_water_potential(element.volume, delta_water_content_ele) #: Dimensions delta_element_dimensions = element.calculate_delta_organ_dimensions(delta_turgor_water_potential, element.organ_dimensions) #: Derivatives y_derivatives[self.initial_conditions_mapping[element]['turgor_water_potential']] = delta_turgor_water_potential if hiddenzone is not None: # Growing leaf if element.is_growing == True: # Growing element y_derivatives[self.initial_conditions_mapping[element]['water_content']] = delta_water_content_ele + delta_water_content_hz # Transfer of water content from hiddenzone # Plastic deformation y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] = delta_hiddenzone_dimensions_plastic['length'] + delta_element_dimensions['length'] y_derivatives[self.initial_conditions_mapping[element]['length']] = delta_hiddenzone_dimensions_plastic['length'] + delta_element_dimensions['length'] y_derivatives[self.initial_conditions_mapping[element]['width']] = delta_hiddenzone_dimensions_plastic['width'] + delta_element_dimensions['width'] y_derivatives[self.initial_conditions_mapping[element]['thickness']] = delta_hiddenzone_dimensions_plastic['thickness'] + delta_element_dimensions['thickness'] y_derivatives[self.initial_conditions_mapping[element]['length']] = delta_hiddenzone_dimensions_plastic['length'] + delta_element_dimensions['length'] elif element.is_growing == False: # End of leaf elongation #: Derivatives y_derivatives[self.initial_conditions_mapping[element]['water_content']] = delta_water_content_ele # Elastic deformation y_derivatives[self.initial_conditions_mapping[element]['length']] = delta_element_dimensions['length'] y_derivatives[self.initial_conditions_mapping[element]['width']] = delta_element_dimensions['width'] y_derivatives[self.initial_conditions_mapping[element]['thickness']] = delta_element_dimensions['thickness'] y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] = delta_element_dimensions['length'] # if hiddenzone.leaf_is_growing == False: if hiddenzone.leaf_pseudo_age >= hiddenzone.PARAMETERS.tend: # Length of HT at the end of elongation - UPDATE VICTORIA 07.01.25 hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) else: # Mature leaf #: Derivatives y_derivatives[self.initial_conditions_mapping[element]['water_content']] = delta_water_content_ele # Elastic deformation y_derivatives[self.initial_conditions_mapping[element]['length']] = delta_element_dimensions['length'] y_derivatives[self.initial_conditions_mapping[element]['width']] = delta_element_dimensions['width'] y_derivatives[self.initial_conditions_mapping[element]['thickness']] = delta_element_dimensions['thickness'] #: Dimensions volume of element element.organ_volume = element.calculate_organ_volume(element.organ_dimensions) element.WC_mstruct = element.water_content / (element.water_content + element.mstruct) * 100 # Store water used from soil for eahc axis soil_water_outputs += (axis.water_influx + axis.Growth) # compute the derivative of each compartment of soil y_derivatives[self.initial_conditions_mapping[soil]['water_content']] = soil.calculate_water_content_derivative(soil_water_outputs, soil.constant_water_content) derivatives_logger = logging.getLogger('hydraulics.derivatives') if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): self._log_compartments(t_abs, y_derivatives, Simulation.LOGGERS_NAMES['derivatives']) return y_derivatives