From d69634baabfcd0ecc321f5c7aa42195cbfd2879b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 27 Aug 2026 15:01:29 +0200 Subject: [PATCH 1/3] added explicit_tillers option and corresponding edits in the submodels --- src/openalea/cnwgrass/cnmetabolism/model.py | 110 ++-- .../cnwgrass/cnmetabolism/simulation.py | 572 +++++++++++------- .../cnwgrass/gasexchange/simulation.py | 15 +- src/openalea/cnwgrass/growth/simulation.py | 539 ++++++++--------- src/openalea/cnwgrass/hydraulics/model.py | 18 + .../cnwgrass/hydraulics/simulation.py | 66 +- .../integration/cnmetabolism_facade.py | 472 +++++++++------ .../cnwgrass/integration/component.py | 31 +- .../integration/gasexchange_facade.py | 13 +- .../cnwgrass/integration/growth_facade.py | 63 +- .../cnwgrass/integration/hydraulics_facade.py | 462 +++++++++----- .../integration/morphogenesis_facade.py | 8 +- src/openalea/cnwgrass/integration/runner.py | 16 +- .../cnwgrass/integration/senescence_facade.py | 26 +- src/openalea/cnwgrass/morphogenesis/model.py | 48 +- .../cnwgrass/morphogenesis/simulation.py | 29 +- .../cnwgrass/senescence/simulation.py | 11 +- 17 files changed, 1527 insertions(+), 972 deletions(-) diff --git a/src/openalea/cnwgrass/cnmetabolism/model.py b/src/openalea/cnwgrass/cnmetabolism/model.py index 7117f39..2d1ed62 100644 --- a/src/openalea/cnwgrass/cnmetabolism/model.py +++ b/src/openalea/cnwgrass/cnmetabolism/model.py @@ -2,7 +2,7 @@ from __future__ import division # use "//" to do integer division import numpy as np -from math import exp +from math import exp, sqrt from openalea.cnwgrass.cnmetabolism import parameters @@ -14,6 +14,33 @@ """ +#: Several consumption/degradation rate formulas branch hard on a substrate being <= 0 (or clamp it with +#: max(0., x)): triosesP (S_Starch, S_Sucrose, S_Amino_Acids), sucrose (Regul_S_Fructan, S_Fructan, D_Fructan) +#: and the fructan stock cap in D_Fructan. For a small/mature element or hiddenzone, a pool's net hourly +#: balance can genuinely land within the ODE solver's own atol (~1E-6) of 0 -- below the precision at which +#: BDF can even resolve its sign. Left as a hard branch, this is a real (non-differentiable) kink exactly +#: where that noise lives; BDF's polynomial predictor can extrapolate through it into a reciprocal-type term +#: and overflow float64. SMOOTHING_EPSILON sets the width of a smooth (C1) transition around 0 instead -- +#: well above solver noise, negligible next to any real substrate pool. +SMOOTHING_EPSILON = 1E-4 #: umol (C or N) + +#: Regul_S_Fructan's hard branch is different in kind from the ones above: it switches on the SIGN of a FLUX +#: (Unloading_Sucrose / Loading_Sucrose), not a substrate pool sitting at the solver's noise floor. CSV data +#: shows this flux genuinely swinging by whole units (observed range roughly -4.5 to +0.5) across real, +#: accepted hourly steps -- the model's own dynamics race through a narrow transition zone far faster than +#: SMOOTHING_EPSILON=1E-4 wide, so smoothing at that scale is mathematically C1 but practically invisible: +#: BDF still experiences an effective jump within a step. This needs a transition width comparable to how far +#: the flux actually moves in a step, not to triosesP's near-zero noise floor -- otherwise S_Fructan keeps +#: swinging between ~0 and its unregulated max (VMAX_SFRUCTAN_POT) in a single hour, which is what produced +#: the HiddenZone[fructan] NaN crashes. +REGUL_SFRUCTAN_SMOOTHING_EPSILON = 0.05 #: umol C h-1 (Unloading_Sucrose / Loading_Sucrose) + + +def _smooth_positive(x, epsilon=SMOOTHING_EPSILON): + """C1-continuous approximation of max(0., x): -> x for x >> epsilon, -> 0 for x << -epsilon, with a smooth + ~epsilon-wide transition around 0 instead of a hard kink.""" + return 0.5 * (x + sqrt(x ** 2 + epsilon ** 2)) + class EcophysiologicalConstants: """ @@ -170,18 +197,26 @@ def __init__(self, label=None, roots=None, phloem=None, grains=None, endosperm=N def calculate_aggregated_variables(self): """Calculate the integrative variables of the axis recursively. + + `self.roots` / `self.phloem` / `self.grains` may be the SAME shared object across several axes of one + plant (main stem + explicit tillers, see cnmetabolism_facade._initialize_model). Their mass is only + folded into `self.mstruct` / `self.nitrates` / `self.senesced_mstruct` for the main stem, which owns + them -- otherwise summing `axis.mstruct` (or `.nitrates`) across a plant's axes would double- (or + N-fold-) count the plant's single below-ground root system / reproductive structure. """ self.mstruct = 0 self.senesced_mstruct = 0 self.nitrates = 0 + is_owner_axis = (self.label == 'MS') if self.roots is not None: - self.roots.calculate_aggregated_variables() - self.mstruct += self.roots.mstruct - self.senesced_mstruct += self.roots.senesced_mstruct - self.nitrates += self.roots.nitrates - if self.phloem is not None: + if is_owner_axis: + self.roots.calculate_aggregated_variables() + self.mstruct += self.roots.mstruct + self.senesced_mstruct += self.roots.senesced_mstruct + self.nitrates += self.roots.nitrates + if self.phloem is not None and is_owner_axis: self.phloem.calculate_aggregated_variables() - if self.grains is not None: + if self.grains is not None and is_owner_axis: self.grains.calculate_aggregated_variables() self.mstruct += self.grains.structural_dry_mass for phytomer in self.phytomers: @@ -532,13 +567,17 @@ def calculate_Regul_S_Fructan(self, Unloading_Sucrose): :rtype: float """ - if Unloading_Sucrose >= 0: - Vmax_Sfructans = HiddenZone.PARAMETERS.VMAX_SFRUCTAN_POT - else: # Regulation by sucrose unloading if hidden zone is a source for C - rate_Loading_Sucrose_massic = -Unloading_Sucrose / self.mstruct / parameters.SECOND_TO_HOUR_RATE_CONVERSION - Vmax_Sfructans = HiddenZone.PARAMETERS.VMAX_SFRUCTAN_POT * (HiddenZone.PARAMETERS.K_REGUL_SFRUCTAN ** HiddenZone.PARAMETERS.N_REGUL_SFRUCTAN / - (max(0., rate_Loading_Sucrose_massic ** HiddenZone.PARAMETERS.N_REGUL_SFRUCTAN) + - HiddenZone.PARAMETERS.K_REGUL_SFRUCTAN ** HiddenZone.PARAMETERS.N_REGUL_SFRUCTAN)) + # Regulation by sucrose unloading if hidden zone is a source for C. `Unloading_Sucrose` is a flux (not a + # substrate pool), and it can genuinely oscillate in sign hour to hour once fructan synthesis itself + # starts drawing enough sucrose to swing the phloem/hiddenzone gradient -- the hard >= 0 branch below + # used to snap between the unregulated VMAX_SFRUCTAN_POT and the regulated rate every time it crossed, + # which can sustain a bang-bang limit cycle. Both branches agree in value exactly at the crossing point + # (rate_Loading_Sucrose_massic == 0 there), so smoothing just that term into a one-sided ramp removes + # the branch without changing behaviour away from the crossing. + rate_Loading_Sucrose_massic = _smooth_positive(-Unloading_Sucrose, REGUL_SFRUCTAN_SMOOTHING_EPSILON) / self.mstruct / parameters.SECOND_TO_HOUR_RATE_CONVERSION + Vmax_Sfructans = HiddenZone.PARAMETERS.VMAX_SFRUCTAN_POT * (HiddenZone.PARAMETERS.K_REGUL_SFRUCTAN ** HiddenZone.PARAMETERS.N_REGUL_SFRUCTAN / + (rate_Loading_Sucrose_massic ** HiddenZone.PARAMETERS.N_REGUL_SFRUCTAN + + HiddenZone.PARAMETERS.K_REGUL_SFRUCTAN ** HiddenZone.PARAMETERS.N_REGUL_SFRUCTAN)) return Vmax_Sfructans def calculate_S_Fructan(self, sucrose, Regul_S_Fructan, T_effect_Vmax): @@ -1444,13 +1483,13 @@ def calculate_Regul_S_Fructan(self, Loading_Sucrose): :return: Maximal rate of fructan synthesis (�mol` C g-1 mstruct h-1) :rtype: float """ - if Loading_Sucrose <= 0: - Vmax_Sfructans = self.__class__.PARAMETERS.VMAX_SFRUCTAN_POT - else: # Regulation by sucrose loading - rate_Loading_Sucrose_massic = Loading_Sucrose / self.mstruct / parameters.SECOND_TO_HOUR_RATE_CONVERSION - Vmax_Sfructans = ((self.__class__.PARAMETERS.VMAX_SFRUCTAN_POT * self.__class__.PARAMETERS.K_REGUL_SFRUCTAN ** self.__class__.PARAMETERS.N_REGUL_SFRUCTAN) / - (max(0, rate_Loading_Sucrose_massic ** self.__class__.PARAMETERS.N_REGUL_SFRUCTAN) + - self.__class__.PARAMETERS.K_REGUL_SFRUCTAN ** self.__class__.PARAMETERS.N_REGUL_SFRUCTAN)) + # Regulation by sucrose loading -- see HiddenZone.calculate_Regul_S_Fructan for why the hard branch on + # the flux's sign is smoothed away instead of kept (same "switch-off" formula, opposite polarity: here + # the unregulated rate applies when Loading_Sucrose <= 0). + rate_Loading_Sucrose_massic = _smooth_positive(Loading_Sucrose, REGUL_SFRUCTAN_SMOOTHING_EPSILON) / self.mstruct / parameters.SECOND_TO_HOUR_RATE_CONVERSION + Vmax_Sfructans = ((self.__class__.PARAMETERS.VMAX_SFRUCTAN_POT * self.__class__.PARAMETERS.K_REGUL_SFRUCTAN ** self.__class__.PARAMETERS.N_REGUL_SFRUCTAN) / + (rate_Loading_Sucrose_massic ** self.__class__.PARAMETERS.N_REGUL_SFRUCTAN + + self.__class__.PARAMETERS.K_REGUL_SFRUCTAN ** self.__class__.PARAMETERS.N_REGUL_SFRUCTAN)) return Vmax_Sfructans @staticmethod @@ -1479,11 +1518,9 @@ def calculate_S_Starch(self, triosesP, T_effect_Vmax): :return: Rate of Starch synthesis (�mol` C g-1 mstruct h-1) :rtype: float """ - if triosesP <= 0: - S_Starch = 0 - else: - S_Starch = (((triosesP / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) * self.__class__.PARAMETERS.VMAX_STARCH) / - ((triosesP / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) + self.__class__.PARAMETERS.K_STARCH)) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax + triosesP_eff = _smooth_positive(triosesP) + S_Starch = (((triosesP_eff / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) * self.__class__.PARAMETERS.VMAX_STARCH) / + ((triosesP_eff / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) + self.__class__.PARAMETERS.K_STARCH)) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax return S_Starch def calculate_D_Starch(self, starch, T_effect_Vmax): @@ -1508,11 +1545,9 @@ def calculate_S_Sucrose(self, triosesP, T_effect_Vmax): :return: Rate of Sucrose synthesis (�mol` C g-1 mstruct h-1) :rtype: float """ - if triosesP <= 0: - S_Sucrose = 0 - else: - S_Sucrose = (((triosesP / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) * self.__class__.PARAMETERS.VMAX_SUCROSE) / - ((triosesP / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) + self.__class__.PARAMETERS.K_SUCROSE)) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax + triosesP_eff = _smooth_positive(triosesP) + S_Sucrose = (((triosesP_eff / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) * self.__class__.PARAMETERS.VMAX_SUCROSE) / + ((triosesP_eff / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) + self.__class__.PARAMETERS.K_SUCROSE)) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax return S_Sucrose def calculate_Loading_Sucrose(self, sucrose, sucrose_phloem, mstruct_axis, T_effect_conductivity): @@ -1637,13 +1672,12 @@ def calculate_S_amino_acids(self, nitrates, triosesP, T_effect_Vmax): :return: Rate of Amino acids synthesis (�mol` N h-1 g-1 mstruct) :rtype: float """ - if nitrates <= 0 or triosesP <= 0: - calculate_S_amino_acids = 0 - else: - calculate_S_amino_acids = self.__class__.PARAMETERS.VMAX_AMINO_ACIDS / \ - ((1 + self.__class__.PARAMETERS.K_AMINO_ACIDS_NITRATES / (nitrates / (self.mstruct * self.__class__.PARAMETERS.ALPHA))) * - (1 + self.__class__.PARAMETERS.K_AMINO_ACIDS_TRIOSESP / (triosesP / (self.mstruct * self.__class__.PARAMETERS.ALPHA)))) * \ - parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax + nitrates_eff = _smooth_positive(nitrates) + triosesP_eff = _smooth_positive(triosesP) + calculate_S_amino_acids = self.__class__.PARAMETERS.VMAX_AMINO_ACIDS / \ + ((1 + self.__class__.PARAMETERS.K_AMINO_ACIDS_NITRATES / (nitrates_eff / (self.mstruct * self.__class__.PARAMETERS.ALPHA))) * + (1 + self.__class__.PARAMETERS.K_AMINO_ACIDS_TRIOSESP / (triosesP_eff / (self.mstruct * self.__class__.PARAMETERS.ALPHA)))) * \ + parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax return calculate_S_amino_acids def calculate_S_proteins(self, amino_acids, T_effect_Vmax): diff --git a/src/openalea/cnwgrass/cnmetabolism/simulation.py b/src/openalea/cnwgrass/cnmetabolism/simulation.py index bc2eac7..3567cc2 100644 --- a/src/openalea/cnwgrass/cnmetabolism/simulation.py +++ b/src/openalea/cnwgrass/cnmetabolism/simulation.py @@ -528,7 +528,11 @@ def initialize(self, population, soils, Tsoil=12): axis.label) logger.exception(message) raise SimulationInitializationError(message) - if not self.external_soil_model and (plant.index, axis.label) not in self.soils: # each axis must be associated to a soil if no external soil model declared + #: axis.roots/phloem may be shared across several axes of one plant (main stem + explicit + #: tillers, see cnmetabolism_facade._initialize_model): only the main stem owns a soil + #: entry -- every soil lookup elsewhere in this module is hardcoded to (plant, 'MS'), so a + #: tiller axis is never expected to have its own dedicated soil. + if not self.external_soil_model and axis.label == 'MS' and (plant.index, axis.label) not in self.soils: # each axis must be associated to a soil if no external soil model declared message = 'No soil found in (plant={},axis={})'.format(plant.index, axis.label) logger.exception(message) @@ -615,20 +619,26 @@ def _init_initial_conditions_roots(root_object, index): i = _init_initial_conditions(plant, i) for axis in plant.axes: i = _init_initial_conditions(axis, i) - for organ in (axis.roots, axis.phloem, axis.grains, axis.endosperm): - if organ is None: - continue - elif organ == axis.roots and self.isolated_roots: - if self.cnwgrass_roots: - i_root = _init_initial_conditions_roots(organ, i_root) - # If roots are not present, an initialization at each time step is not necessary - else: - if self.first_initialization: + #: axis.roots/phloem/grains/endosperm may be the SAME shared object across several axes of one + #: plant (main stem + explicit tillers, see cnmetabolism_facade._initialize_model): give them + #: ODE state-vector indices ONCE per plant (on the main stem, which owns them) -- otherwise each + #: sharing axis would append a further duplicate (and ultimately orphaned) set of slots to the + #: state vector, and only the last-built duplicate's indices would actually be used. + if axis.label == 'MS': + for organ in (axis.roots, axis.phloem, axis.grains, axis.endosperm): + if organ is None: + continue + elif organ == axis.roots and self.isolated_roots: + if self.cnwgrass_roots: i_root = _init_initial_conditions_roots(organ, i_root) - self.first_initialization = False + # If roots are not present, an initialization at each time step is not necessary + else: + if self.first_initialization: + i_root = _init_initial_conditions_roots(organ, i_root) + self.first_initialization = False - else: - i = _init_initial_conditions(organ, i) + else: + i = _init_initial_conditions(organ, i) for phytomer in axis.phytomers: i = _init_initial_conditions(phytomer, i) for organ in (phytomer.chaff, phytomer.peduncle, phytomer.lamina, phytomer.internode, phytomer.sheath, phytomer.hiddenzone): @@ -646,6 +656,18 @@ def _init_initial_conditions_roots(root_object, index): logger.info('Initialization of the simulation DONE') + @staticmethod + def _describe_nan(y, mapping): + """Return a list of ``'[]=NaN'`` strings identifying which compartments of + `mapping` (an :attr:`initial_conditions_mapping`-like dict) are NaN in `y`, for error reporting.""" + nan_indices = set(np.nonzero(np.isnan(y))[0]) + offenders = [] + for model_object, compartments in mapping.items(): + for compartment_name, index in compartments.items(): + if index in nan_indices: + offenders.append('{}[{}]=NaN'.format(model_object, compartment_name)) + return offenders + def run(self): """ Compute CN exchanges which occurred in :attr:`population` and :attr:`soils` over :attr:`delta_t`. @@ -662,39 +684,68 @@ def run(self): if self.isolated_roots and self.cnwgrass_roots: self._update_initial_conditions_roots() + #: axis.roots.Export_cytokinins is computed by _calculate_shoot_derivatives itself, from + #: axis.roots.cytokinins and regul_transpiration -- but regul_transpiration needs plant_Total_Transpiration, + #: which is only fully known after the WHOLE per-axis/per-element loop has run. So Export_cytokinins is + #: necessarily computed late in that function, while each element's cytokinins_import (needing that + #: same Export_cytokinins to split the flux across elements) is computed earlier, in the same loop that's + #: still accumulating the total -- a circular dependency within one forward pass. fun(t, y) passed to + #: solve_ivp must be a pure function of (t, y) for BDF/num_jac's repeated same-(t, y) Jacobian probing to + #: behave correctly, so the value is frozen once here, before the solve starts, and read from that + #: snapshot at the element-import site instead of the live attribute -- an explicit, deliberate one-step + #: lag, the same pattern already used for Tr/green_area/Export_Nitrates/Export_Amino_Acids. + for plant in self.population.plants: + for axis in plant.axes: + if axis.roots is not None: + axis.roots._Export_cytokinins_frozen = axis.roots.Export_cytokinins + 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 during 1 time step ; # :func:`scipy.integrate.solve_ivp` computes the derivatives of each function by calling :meth:`_calculate_all_derivatives` + #: max_step is left unconstrained (BDF's own adaptive step control) rather than capped to a fraction of + #: time_step: the discontinuities that used to require a tiny step cap -- the triosesP noise-floor kink, + #: the Regul_S_Fructan bang-bang switch, and the Export_cytokinins purity bug (non-deterministic + #: fun(t, y)) -- are now fixed structurally (see _smooth_positive usages above and the Export_cytokinins + #: snapshot above), so BDF no longer needs to be forced into small steps every hour regardless of the + #: dynamics' real stiffness that hour. + max_step = np.inf if not self.isolated_roots: sol = solve_ivp(fun=self._calculate_all_derivatives, t_span=self.time_grid, y0=self.initial_conditions, - method='BDF', t_eval=np.array([self.time_step]), dense_output=False) + method='BDF', t_eval=np.array([self.time_step]), dense_output=False, max_step=max_step) elif self.cnwgrass_roots: sol = solve_ivp(fun=self._calculate_shoot_derivatives, t_span=self.time_grid, y0=self.initial_conditions, - method='BDF', t_eval=None, dense_output=False) + method='BDF', t_eval=None, dense_output=False, max_step=max_step) sol_root = solve_ivp(fun=self._calculate_root_derivatives, t_span=self.time_grid, y0=self.initial_conditions_roots, - method='BDF', t_eval=None, dense_output=False) - + method='BDF', t_eval=None, dense_output=False, max_step=max_step) + else: sol = solve_ivp(fun=self._calculate_shoot_derivatives, t_span=self.time_grid, y0=self.initial_conditions, method='BDF', t_eval=np.array([self.time_step]), dense_output=False, - # jac_sparsity=self._jac_sparsity_shoot, + jac_sparsity=self._jac_sparsity_shoot, + max_step=max_step ) 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) + if self.isolated_roots and self.cnwgrass_roots: + if not sol_root.success: + message = "Integration failed: {}".format(sol_root.message) + logger.exception(message) + raise SimulationRunError(message) + # Re-compute integrative variables self.population.calculate_aggregated_variables() @@ -897,7 +948,8 @@ def _calculate_all_derivatives(self, t, y): # 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' + offenders = self._describe_nan(y, self.initial_conditions_mapping) + message = 'The solver did not manage to compute a compartment. See the logs. NaN found in y: {}'.format('; '.join(offenders)) logger.exception(message) raise SimulationRunError(message) @@ -912,50 +964,34 @@ def _calculate_all_derivatives(self, t, y): soil.T_effect_conductivity = soil.calculate_temperature_effect_on_conductivity(soil.Tsoil) for plant in self.population.plants: + #: axis.roots / axis.phloem / axis.grains / axis.endosperm may be the SAME shared object across + #: several axes of one plant (main stem + explicit tillers, see + #: cnmetabolism_facade._initialize_model): their regulatory and derivative equations are only + #: computed ONCE per plant below (on the main stem, which owns them) -- not once per axis, which + #: would silently overwrite a shared ODE derivative or double-count the plant's soil N uptake. + #: Wherever such an equation feeds a pool shared across axes, it is normalized against + #: plant-aggregated transpiration/mstruct (summed over every axis of the plant) instead of one + #: axis's own value, so that e.g. the sum of every element's nitrate import still equals + #: axis.roots.Export_Nitrates exactly once, rather than once per axis. + ms_axis = None + plant_Total_Transpiration = 0.0 # mmol s-1, summed across every axis of the plant + plant_mstruct = 0.0 # g, summed across every axis of the plant + for axis in plant.axes: axis.T_effect_conductivity = plant.calculate_temperature_effect_on_conductivity(axis.SAM_temperature) axis.T_effect_Vmax = plant.calculate_temperature_effect_on_Vmax(axis.SAM_temperature) + if axis.label == 'MS': + ms_axis = axis - # Phloem - phloem_contributors = [] axis.phloem.sucrose = y[self.initial_conditions_mapping[axis.phloem]['sucrose']] axis.phloem.amino_acids = y[self.initial_conditions_mapping[axis.phloem]['amino_acids']] - # Endosperm - empty_endosperm = True - if axis.endosperm is not None and ((axis.endosperm.starch / axis.endosperm.PARAMETERS.STARCH_MAX) > 0.01 or (axis.endosperm.proteins / axis.endosperm.PARAMETERS.PROTEINS_MAX) > 0.01): - empty_endosperm = False - axis.endosperm.moistening = y[self.initial_conditions_mapping[axis.endosperm]['moistening']] - if axis.endosperm.moistening < 1: - y_derivatives[self.initial_conditions_mapping[axis.endosperm]['moistening']] = axis.endosperm.calculate_moistening() - continue - else: - axis.endosperm.starch = y[self.initial_conditions_mapping[axis.endosperm]['starch']] - axis.endosperm.proteins = y[self.initial_conditions_mapping[axis.endosperm]['proteins']] - phloem_contributors.append(axis.endosperm) - - # intermediate variables - T_effect_Vmax = axis.endosperm.calculate_temperature_effect_on_growth(soil.Tsoil) - - # flows - axis.endosperm.D_starch = axis.endosperm.calculate_D_starch(axis.endosperm.starch, T_effect_Vmax) - axis.endosperm.D_proteins = axis.endosperm.calculate_D_proteins(axis.endosperm.proteins, T_effect_Vmax) - - # compartments derivatives - axis.endosperm.R_residual = self.respiration_model.RespirationModel.R_endosperm(axis.endosperm.starch, axis.endosperm.mstruct, soil.Tsoil) - starch_derivative = axis.endosperm.calculate_starch_derivative(axis.endosperm.D_starch, axis.endosperm.R_residual) - proteins_derivative = axis.endosperm.calculate_proteins_derivative(axis.endosperm.D_proteins) - y_derivatives[self.initial_conditions_mapping[axis.endosperm]['starch']] = starch_derivative - y_derivatives[self.initial_conditions_mapping[axis.endosperm]['proteins']] = proteins_derivative - - # Roots axis.roots.nitrates = y[self.initial_conditions_mapping[axis.roots]['nitrates']] axis.roots.amino_acids = y[self.initial_conditions_mapping[axis.roots]['amino_acids']] axis.roots.sucrose = y[self.initial_conditions_mapping[axis.roots]['sucrose']] axis.roots.cytokinins = y[self.initial_conditions_mapping[axis.roots]['cytokinins']] - phloem_contributors.append(axis.roots) - # compute total transpiration at t_inf + # compute total transpiration at t_inf, for this axis and accumulated at plant scale axis.Total_Transpiration = 0.0 # mmol s-1 total_green_area = 0.0 # m2 for phytomer in axis.phytomers: @@ -966,24 +1002,65 @@ def _calculate_all_derivatives(self, t, y): element.Transpiration = element.calculate_Total_Transpiration(element.Tr, element.green_area) axis.Total_Transpiration += (element.Transpiration * element.nb_replications) total_green_area += (element.green_area * element.nb_replications) + plant_Total_Transpiration += axis.Total_Transpiration + plant_mstruct += axis.mstruct + + axis = ms_axis #: owns the (possibly shared) roots/phloem/grains/endosperm from here on + + # Phloem contributors, merged across every axis of the plant + phloem_contributors = [] + + # Endosperm (shared: computed once, on the main stem) + empty_endosperm = True + germinating = False + if axis.endosperm is not None and ((axis.endosperm.starch / axis.endosperm.PARAMETERS.STARCH_MAX) > 0.01 or (axis.endosperm.proteins / axis.endosperm.PARAMETERS.PROTEINS_MAX) > 0.01): + empty_endosperm = False + axis.endosperm.moistening = y[self.initial_conditions_mapping[axis.endosperm]['moistening']] + if axis.endosperm.moistening < 1: + y_derivatives[self.initial_conditions_mapping[axis.endosperm]['moistening']] = axis.endosperm.calculate_moistening() + germinating = True + else: + phloem_contributors.append(axis.endosperm) - # Compute the regulating factor of root exports by shoot transpiration - axis.roots.regul_transpiration = axis.roots.calculate_regul_transpiration(axis.Total_Transpiration) + # intermediate variables + T_effect_Vmax = axis.endosperm.calculate_temperature_effect_on_growth(soil.Tsoil) - # compute the flows from/to the roots to/from photosynthetic organs - if not self.external_soil_model: - axis.roots.Uptake_Nitrates, axis.roots.HATS_LATS = axis.roots.calculate_Uptake_Nitrates(soil.Conc_Nitrates_Soil, axis.roots.nitrates, axis.roots.sucrose, - soil.T_effect_Vmax, soil.SRWC) - soil_contributors.append((axis.roots.Uptake_Nitrates, plant.index)) #: TODO TEMP!!! - else: - assert axis.roots is not None - axis.roots.R_Nnit_upt = self.respiration_model.RespirationModel.R_Nnit_upt(axis.roots.Uptake_Nitrates, axis.roots.sucrose) - axis.roots.Export_Nitrates = axis.roots.calculate_Export_Nitrates(axis.roots.nitrates, axis.roots.regul_transpiration) - axis.roots.Export_Amino_Acids = axis.roots.calculate_Export_Amino_Acids(axis.roots.amino_acids, axis.roots.regul_transpiration) - axis.roots.Export_cytokinins = axis.roots.calculate_Export_cytokinins(axis.roots.cytokinins, axis.roots.regul_transpiration) + # flows + axis.endosperm.D_starch = axis.endosperm.calculate_D_starch(axis.endosperm.starch, T_effect_Vmax) + axis.endosperm.D_proteins = axis.endosperm.calculate_D_proteins(axis.endosperm.proteins, T_effect_Vmax) - # compute the derivative of each photosynthetic organ element compartment - for phytomer in axis.phytomers: + # compartments derivatives + axis.endosperm.R_residual = self.respiration_model.RespirationModel.R_endosperm(axis.endosperm.starch, axis.endosperm.mstruct, soil.Tsoil) + starch_derivative = axis.endosperm.calculate_starch_derivative(axis.endosperm.D_starch, axis.endosperm.R_residual) + proteins_derivative = axis.endosperm.calculate_proteins_derivative(axis.endosperm.D_proteins) + y_derivatives[self.initial_conditions_mapping[axis.endosperm]['starch']] = starch_derivative + y_derivatives[self.initial_conditions_mapping[axis.endosperm]['proteins']] = proteins_derivative + + if germinating: + #: Matches the original single-axis behaviour: while the endosperm is still moistening, no + #: roots/phloem/phytomer derivative is computed at all this call for this plant. + continue + + phloem_contributors.append(axis.roots) + + # Compute the regulating factor of root exports by whole-plant shoot transpiration + axis.roots.regul_transpiration = axis.roots.calculate_regul_transpiration(plant_Total_Transpiration) + + # compute the flows from/to the roots to/from photosynthetic organs + if not self.external_soil_model: + axis.roots.Uptake_Nitrates, axis.roots.HATS_LATS = axis.roots.calculate_Uptake_Nitrates(soil.Conc_Nitrates_Soil, axis.roots.nitrates, axis.roots.sucrose, + soil.T_effect_Vmax, soil.SRWC) + soil_contributors.append((axis.roots.Uptake_Nitrates, plant.index)) #: safe: axis is ms_axis here (reassigned above), so this runs once per plant even when roots are shared across tillers + else: + assert axis.roots is not None + axis.roots.R_Nnit_upt = self.respiration_model.RespirationModel.R_Nnit_upt(axis.roots.Uptake_Nitrates, axis.roots.sucrose) + axis.roots.Export_Nitrates = axis.roots.calculate_Export_Nitrates(axis.roots.nitrates, axis.roots.regul_transpiration) + axis.roots.Export_Amino_Acids = axis.roots.calculate_Export_Amino_Acids(axis.roots.amino_acids, axis.roots.regul_transpiration) + axis.roots.Export_cytokinins = axis.roots.calculate_Export_cytokinins(axis.roots.cytokinins, axis.roots.regul_transpiration) + + # compute the derivative of each photosynthetic organ element compartment, for every axis in turn + for shoot_axis in plant.axes: + for phytomer in shoot_axis.phytomers: # Hidden zone hiddenzone = phytomer.hiddenzone if phytomer.hiddenzone is not None: @@ -1027,8 +1104,8 @@ def _calculate_all_derivatives(self, t, y): else: #: Loading of sucrose and amino acids towards the phloem phloem_contributors.append(element) - element.Loading_Sucrose = element.calculate_Loading_Sucrose(element.sucrose, axis.phloem.sucrose, axis.mstruct, element.T_effect_conductivity) - element.Loading_Amino_Acids = element.calculate_Loading_Amino_Acids(element.amino_acids, axis.phloem.amino_acids, axis.mstruct, element.T_effect_conductivity) + element.Loading_Sucrose = element.calculate_Loading_Sucrose(element.sucrose, axis.phloem.sucrose, plant_mstruct, element.T_effect_conductivity) + element.Loading_Amino_Acids = element.calculate_Loading_Amino_Acids(element.amino_acids, axis.phloem.amino_acids, plant_mstruct, element.T_effect_conductivity) element.Regul_S_Fructan = element.calculate_Regul_S_Fructan(element.Loading_Sucrose) element.S_Fructan = element.calculate_S_Fructan(element.sucrose, element.Regul_S_Fructan, element.T_effect_Vmax) @@ -1038,14 +1115,14 @@ def _calculate_all_derivatives(self, t, y): element.S_Sucrose = element.calculate_S_Sucrose(element.triosesP, element.T_effect_Vmax) element.R_phloem_loading, element.Loading_Sucrose = self.respiration_model.RespirationModel.R_phloem(element.Loading_Sucrose, element.mstruct * element.__class__.PARAMETERS.ALPHA) - element.Nitrates_import = element.calculate_Nitrates_import(axis.roots.Export_Nitrates, element.Transpiration, axis.Total_Transpiration) - element.Amino_Acids_import = element.calculate_Amino_Acids_import(axis.roots.Export_Amino_Acids, element.Transpiration, axis.Total_Transpiration) + element.Nitrates_import = element.calculate_Nitrates_import(axis.roots.Export_Nitrates, element.Transpiration, plant_Total_Transpiration) + element.Amino_Acids_import = element.calculate_Amino_Acids_import(axis.roots.Export_Amino_Acids, element.Transpiration, plant_Total_Transpiration) element.S_Amino_Acids = element.calculate_S_amino_acids(element.nitrates, element.triosesP, element.T_effect_Vmax) element.R_Nnit_red, element.S_Amino_Acids = self.respiration_model.RespirationModel.R_Nnit_red(element.S_Amino_Acids, element.sucrose, element.mstruct * element.__class__.PARAMETERS.ALPHA) element.S_Proteins = element.calculate_S_proteins(element.amino_acids, element.T_effect_Vmax) element.D_Proteins = element.calculate_D_Proteins(element.proteins, element.cytokinins, element.T_effect_Vmax) - element.cytokinins_import = element.calculate_cytokinins_import(axis.roots.Export_cytokinins, element.Transpiration, axis.Total_Transpiration) + element.cytokinins_import = element.calculate_cytokinins_import(axis.roots.Export_cytokinins, element.Transpiration, plant_Total_Transpiration) element.D_cytokinins = element.calculate_D_cytokinins(element.cytokinins, element.T_effect_Vmax) # compartments derivatives @@ -1074,29 +1151,29 @@ def _calculate_all_derivatives(self, t, y): if phytomer.hiddenzone is not None: # Unloading of sucrose from phloem - hiddenzone.Unloading_Sucrose = hiddenzone.calculate_Unloading_Sucrose(hiddenzone.sucrose, axis.phloem.sucrose, axis.mstruct, axis.T_effect_conductivity) + hiddenzone.Unloading_Sucrose = hiddenzone.calculate_Unloading_Sucrose(hiddenzone.sucrose, axis.phloem.sucrose, plant_mstruct, shoot_axis.T_effect_conductivity) # Unloading of AA from phloem - hiddenzone.Unloading_Amino_Acids = hiddenzone.calculate_Unloading_Amino_Acids(hiddenzone.amino_acids, axis.phloem.amino_acids, axis.mstruct, axis.T_effect_conductivity) + hiddenzone.Unloading_Amino_Acids = hiddenzone.calculate_Unloading_Amino_Acids(hiddenzone.amino_acids, axis.phloem.amino_acids, plant_mstruct, shoot_axis.T_effect_conductivity) # Fructan synthesis Regul_Sfructanes = hiddenzone.calculate_Regul_S_Fructan(hiddenzone.Unloading_Sucrose) - hiddenzone.S_Fructan = hiddenzone.calculate_S_Fructan(hiddenzone.sucrose, Regul_Sfructanes, axis.T_effect_Vmax) + hiddenzone.S_Fructan = hiddenzone.calculate_S_Fructan(hiddenzone.sucrose, Regul_Sfructanes, shoot_axis.T_effect_Vmax) # Fructan degradation - hiddenzone.D_Fructan = hiddenzone.calculate_D_Fructan(hiddenzone.sucrose, hiddenzone.fructan, axis.T_effect_Vmax) + hiddenzone.D_Fructan = hiddenzone.calculate_D_Fructan(hiddenzone.sucrose, hiddenzone.fructan, shoot_axis.T_effect_Vmax) # Synthesis proteins - hiddenzone.S_Proteins = hiddenzone.calculate_S_proteins(hiddenzone.amino_acids, axis.T_effect_Vmax) + hiddenzone.S_Proteins = hiddenzone.calculate_S_proteins(hiddenzone.amino_acids, shoot_axis.T_effect_Vmax) # Degradation proteins - hiddenzone.D_Proteins = hiddenzone.calculate_D_Proteins(hiddenzone.proteins, axis.T_effect_Vmax) + hiddenzone.D_Proteins = hiddenzone.calculate_D_Proteins(hiddenzone.proteins, shoot_axis.T_effect_Vmax) # Residual respiration hiddenzone.R_residual = self.respiration_model.RespirationModel.R_residual(hiddenzone.sucrose, hiddenzone.mstruct * hiddenzone.__class__.PARAMETERS.ALPHA, hiddenzone.Total_Organic_Nitrogen, - axis.SAM_temperature) + shoot_axis.SAM_temperature) # compute the derivatives of the hidden zone y_derivatives[self.initial_conditions_mapping[hiddenzone]['sucrose']] = hiddenzone.calculate_sucrose_derivative(hiddenzone.Unloading_Sucrose, hiddenzone.S_Fructan, @@ -1108,64 +1185,64 @@ def _calculate_all_derivatives(self, t, y): y_derivatives[self.initial_conditions_mapping[hiddenzone]['fructan']] = hiddenzone.calculate_fructan_derivative(hiddenzone.S_Fructan, hiddenzone.D_Fructan) y_derivatives[self.initial_conditions_mapping[hiddenzone]['proteins']] = hiddenzone.calculate_proteins_derivative(hiddenzone.S_Proteins, hiddenzone.D_Proteins) - if axis.grains is not None: - phloem_contributors.append(axis.grains) - # compute the derivative of each compartment of grains - axis.grains.structure = y[self.initial_conditions_mapping[axis.grains]['structure']] - axis.grains.starch = y[self.initial_conditions_mapping[axis.grains]['starch']] - axis.grains.proteins = y[self.initial_conditions_mapping[axis.grains]['proteins']] - axis.grains.age_from_flowering = y[self.initial_conditions_mapping[axis.grains]['age_from_flowering']] + if axis.grains is not None: + phloem_contributors.append(axis.grains) + # compute the derivative of each compartment of grains + axis.grains.structure = y[self.initial_conditions_mapping[axis.grains]['structure']] + axis.grains.starch = y[self.initial_conditions_mapping[axis.grains]['starch']] + axis.grains.proteins = y[self.initial_conditions_mapping[axis.grains]['proteins']] + axis.grains.age_from_flowering = y[self.initial_conditions_mapping[axis.grains]['age_from_flowering']] - # intermediate variables - T_effect_growth = axis.grains.calculate_temperature_effect_on_growth(axis.SAM_temperature) - axis.grains.structural_dry_mass = axis.grains.calculate_structural_dry_mass(axis.grains.structure) + # intermediate variables + T_effect_growth = axis.grains.calculate_temperature_effect_on_growth(axis.SAM_temperature) + axis.grains.structural_dry_mass = axis.grains.calculate_structural_dry_mass(axis.grains.structure) - # flows - axis.grains.S_grain_structure = axis.grains.calculate_S_grain_structure(axis.grains.structure, axis.phloem.sucrose, axis.mstruct, T_effect_growth) - axis.grains.S_grain_starch = axis.grains.calculate_S_grain_starch(axis.phloem.sucrose, axis.mstruct, axis.T_effect_Vmax) - axis.grains.S_Proteins = axis.grains.calculate_S_proteins(axis.grains.S_grain_structure, axis.grains.S_grain_starch, axis.phloem.amino_acids, axis.phloem.sucrose, - axis.grains.structural_dry_mass) - # compartments derivatives - axis.grains.R_grain_growth_struct, axis.grains.R_grain_growth_starch = self.respiration_model.RespirationModel.R_grain_growth(axis.grains.S_grain_structure, - axis.grains.S_grain_starch, - axis.grains.structural_dry_mass) - structure_derivative = axis.grains.calculate_structure_derivative(axis.grains.S_grain_structure, axis.grains.R_grain_growth_struct) - starch_derivative = axis.grains.calculate_starch_derivative(axis.grains.S_grain_starch, axis.grains.structural_dry_mass, axis.grains.R_grain_growth_starch) - proteins_derivative = axis.grains.calculate_proteins_derivative(axis.grains.S_Proteins) - y_derivatives[self.initial_conditions_mapping[axis.grains]['structure']] = structure_derivative - y_derivatives[self.initial_conditions_mapping[axis.grains]['starch']] = starch_derivative - y_derivatives[self.initial_conditions_mapping[axis.grains]['proteins']] = proteins_derivative - y_derivatives[self.initial_conditions_mapping[axis.grains]['age_from_flowering']] += (self.delta_t * T_effect_growth) # TODO: create a function - - # compute the derivative of each compartment of roots # flows - axis.roots.Unloading_Sucrose = axis.roots.calculate_Unloading_Sucrose(axis.roots.sucrose, axis.phloem.sucrose, axis.mstruct, axis.T_effect_conductivity, axis.nb_leaves) - axis.roots.Unloading_Amino_Acids = axis.roots.calculate_Unloading_Amino_Acids(axis.roots.amino_acids, axis.phloem.amino_acids, axis.phloem.sucrose, axis.roots.Unloading_Sucrose, axis.mstruct, axis.T_effect_conductivity) - axis.roots.S_Amino_Acids = axis.roots.calculate_S_amino_acids(axis.roots.nitrates, axis.roots.sucrose, soil.T_effect_Vmax) - axis.roots.R_Nnit_red, axis.roots.S_Amino_Acids = self.respiration_model.RespirationModel.R_Nnit_red(axis.roots.S_Amino_Acids, axis.roots.sucrose, - axis.roots.mstruct * model.Roots.PARAMETERS.ALPHA, root=True) - axis.roots.C_exudation, axis.roots.N_exudation = axis.roots.calculate_exudation(axis.roots.Unloading_Sucrose, axis.roots.sucrose, axis.roots.amino_acids, axis.phloem.amino_acids) - axis.roots.S_cytokinins = axis.roots.calculate_S_cytokinins(axis.roots.sucrose, axis.roots.nitrates, soil.T_effect_Vmax) - + axis.grains.S_grain_structure = axis.grains.calculate_S_grain_structure(axis.grains.structure, axis.phloem.sucrose, plant_mstruct, T_effect_growth) + axis.grains.S_grain_starch = axis.grains.calculate_S_grain_starch(axis.phloem.sucrose, plant_mstruct, axis.T_effect_Vmax) + axis.grains.S_Proteins = axis.grains.calculate_S_proteins(axis.grains.S_grain_structure, axis.grains.S_grain_starch, axis.phloem.amino_acids, axis.phloem.sucrose, + axis.grains.structural_dry_mass) # compartments derivatives - axis.roots.R_residual = self.respiration_model.RespirationModel.R_residual(axis.roots.sucrose, axis.roots.mstruct * model.Roots.PARAMETERS.ALPHA, axis.roots.Total_Organic_Nitrogen, - soil.Tsoil) - axis.roots.sum_respi = axis.roots.R_Nnit_upt + axis.roots.R_Nnit_red + axis.roots.R_residual - sucrose_derivative = axis.roots.calculate_sucrose_derivative(axis.roots.Unloading_Sucrose, axis.roots.S_Amino_Acids, axis.roots.C_exudation, axis.roots.sum_respi) - nitrates_derivative = axis.roots.calculate_nitrates_derivative(axis.roots.Uptake_Nitrates, axis.roots.Export_Nitrates, axis.roots.S_Amino_Acids) - amino_acids_derivative = axis.roots.calculate_amino_acids_derivative(axis.roots.Unloading_Amino_Acids, axis.roots.S_Amino_Acids, axis.roots.Export_Amino_Acids, axis.roots.N_exudation) - cytokinins_derivative = axis.roots.calculate_cytokinins_derivative(axis.roots.S_cytokinins, axis.roots.Export_cytokinins, axis.roots.cytokinins, empty_endosperm) - - y_derivatives[self.initial_conditions_mapping[axis.roots]['sucrose']] = sucrose_derivative - y_derivatives[self.initial_conditions_mapping[axis.roots]['nitrates']] = nitrates_derivative - y_derivatives[self.initial_conditions_mapping[axis.roots]['amino_acids']] = amino_acids_derivative - y_derivatives[self.initial_conditions_mapping[axis.roots]['cytokinins']] = cytokinins_derivative - - # compute the derivative of each compartment of phloem - sucrose_phloem_derivative = axis.phloem.calculate_sucrose_derivative(phloem_contributors) - amino_acids_phloem_derivative = axis.phloem.calculate_amino_acids_derivative(phloem_contributors) - y_derivatives[self.initial_conditions_mapping[axis.phloem]['sucrose']] = sucrose_phloem_derivative - y_derivatives[self.initial_conditions_mapping[axis.phloem]['amino_acids']] = amino_acids_phloem_derivative + axis.grains.R_grain_growth_struct, axis.grains.R_grain_growth_starch = self.respiration_model.RespirationModel.R_grain_growth(axis.grains.S_grain_structure, + axis.grains.S_grain_starch, + axis.grains.structural_dry_mass) + structure_derivative = axis.grains.calculate_structure_derivative(axis.grains.S_grain_structure, axis.grains.R_grain_growth_struct) + starch_derivative = axis.grains.calculate_starch_derivative(axis.grains.S_grain_starch, axis.grains.structural_dry_mass, axis.grains.R_grain_growth_starch) + proteins_derivative = axis.grains.calculate_proteins_derivative(axis.grains.S_Proteins) + y_derivatives[self.initial_conditions_mapping[axis.grains]['structure']] = structure_derivative + y_derivatives[self.initial_conditions_mapping[axis.grains]['starch']] = starch_derivative + y_derivatives[self.initial_conditions_mapping[axis.grains]['proteins']] = proteins_derivative + y_derivatives[self.initial_conditions_mapping[axis.grains]['age_from_flowering']] += (self.delta_t * T_effect_growth) # TODO: create a function + + # compute the derivative of each compartment of roots + # flows + axis.roots.Unloading_Sucrose = axis.roots.calculate_Unloading_Sucrose(axis.roots.sucrose, axis.phloem.sucrose, plant_mstruct, axis.T_effect_conductivity, axis.nb_leaves) + axis.roots.Unloading_Amino_Acids = axis.roots.calculate_Unloading_Amino_Acids(axis.roots.amino_acids, axis.phloem.amino_acids, axis.phloem.sucrose, axis.roots.Unloading_Sucrose, plant_mstruct, axis.T_effect_conductivity) + axis.roots.S_Amino_Acids = axis.roots.calculate_S_amino_acids(axis.roots.nitrates, axis.roots.sucrose, soil.T_effect_Vmax) + axis.roots.R_Nnit_red, axis.roots.S_Amino_Acids = self.respiration_model.RespirationModel.R_Nnit_red(axis.roots.S_Amino_Acids, axis.roots.sucrose, + axis.roots.mstruct * model.Roots.PARAMETERS.ALPHA, root=True) + axis.roots.C_exudation, axis.roots.N_exudation = axis.roots.calculate_exudation(axis.roots.Unloading_Sucrose, axis.roots.sucrose, axis.roots.amino_acids, axis.phloem.amino_acids) + axis.roots.S_cytokinins = axis.roots.calculate_S_cytokinins(axis.roots.sucrose, axis.roots.nitrates, soil.T_effect_Vmax) + + # compartments derivatives + axis.roots.R_residual = self.respiration_model.RespirationModel.R_residual(axis.roots.sucrose, axis.roots.mstruct * model.Roots.PARAMETERS.ALPHA, axis.roots.Total_Organic_Nitrogen, + soil.Tsoil) + axis.roots.sum_respi = axis.roots.R_Nnit_upt + axis.roots.R_Nnit_red + axis.roots.R_residual + sucrose_derivative = axis.roots.calculate_sucrose_derivative(axis.roots.Unloading_Sucrose, axis.roots.S_Amino_Acids, axis.roots.C_exudation, axis.roots.sum_respi) + nitrates_derivative = axis.roots.calculate_nitrates_derivative(axis.roots.Uptake_Nitrates, axis.roots.Export_Nitrates, axis.roots.S_Amino_Acids) + amino_acids_derivative = axis.roots.calculate_amino_acids_derivative(axis.roots.Unloading_Amino_Acids, axis.roots.S_Amino_Acids, axis.roots.Export_Amino_Acids, axis.roots.N_exudation) + cytokinins_derivative = axis.roots.calculate_cytokinins_derivative(axis.roots.S_cytokinins, axis.roots.Export_cytokinins, axis.roots.cytokinins, empty_endosperm) + + y_derivatives[self.initial_conditions_mapping[axis.roots]['sucrose']] = sucrose_derivative + y_derivatives[self.initial_conditions_mapping[axis.roots]['nitrates']] = nitrates_derivative + y_derivatives[self.initial_conditions_mapping[axis.roots]['amino_acids']] = amino_acids_derivative + y_derivatives[self.initial_conditions_mapping[axis.roots]['cytokinins']] = cytokinins_derivative + + # compute the derivative of each compartment of phloem (once per plant, from every axis's contributors) + sucrose_phloem_derivative = axis.phloem.calculate_sucrose_derivative(phloem_contributors) + amino_acids_phloem_derivative = axis.phloem.calculate_amino_acids_derivative(phloem_contributors) + y_derivatives[self.initial_conditions_mapping[axis.phloem]['sucrose']] = sucrose_phloem_derivative + y_derivatives[self.initial_conditions_mapping[axis.phloem]['amino_acids']] = amino_acids_phloem_derivative if not self.external_soil_model: # compute the derivative of each compartment of soil @@ -1241,53 +1318,32 @@ def _calculate_shoot_derivatives(self, t, y): # 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' + offenders = self._describe_nan(y, self.initial_conditions_mapping) + message = 'The solver did not manage to compute a compartment. See the logs. NaN found in y: {}'.format('; '.join(offenders)) logger.exception(message) raise SimulationRunError(message) y_derivatives = np.zeros_like(y) for plant in self.population.plants: + #: See the equivalent note in _calculate_all_derivatives: axis.roots/phloem/grains/endosperm may be + #: shared across several axes of one plant (main stem + explicit tillers), so their regulatory and + #: derivative equations are computed ONCE per plant below (on the main stem), normalized against + #: plant-aggregated transpiration/mstruct rather than one axis's own value. + ms_axis = None + plant_Total_Transpiration = 0.0 # mmol s-1, summed across every axis of the plant + plant_mstruct = 0.0 # g, summed across every axis of the plant + for axis in plant.axes: axis.T_effect_conductivity = plant.calculate_temperature_effect_on_conductivity(axis.SAM_temperature) axis.T_effect_Vmax = plant.calculate_temperature_effect_on_Vmax(axis.SAM_temperature) + if axis.label == 'MS': + ms_axis = axis - # Phloem - phloem_contributors = axis._phloem_contributors # prebuilt axis.phloem.sucrose = y[axis.phloem._i_sucrose] axis.phloem.amino_acids = y[axis.phloem._i_amino_acids] - # Endosperm - empty_endosperm = True - if axis.endosperm is not None and ((axis.endosperm.starch / axis.endosperm.PARAMETERS.STARCH_MAX) > 0.01 or (axis.endosperm.proteins / axis.endosperm.PARAMETERS.PROTEINS_MAX) > 0.01): - empty_endosperm = False - axis.endosperm.moistening = y[axis.endosperm._i_moistening] # TODO - if axis.endosperm.moistening < 1: - y_derivatives[axis.endosperm._i_moistening] = axis.endosperm.calculate_moistening() - continue - else: - axis.endosperm.starch = y[axis.endosperm._i_starch] - axis.endosperm.proteins = y[axis.endosperm._i_proteins] - # phloem_contributors.append(axis.endosperm) - - # intermediate variables - T_effect_Vmax = axis.endosperm.calculate_temperature_effect_on_growth(soil.Tsoil) - - # flows - axis.endosperm.D_starch = axis.endosperm.calculate_D_starch(axis.endosperm.starch, T_effect_Vmax) - axis.endosperm.D_proteins = axis.endosperm.calculate_D_proteins(axis.endosperm.proteins, T_effect_Vmax) - - # compartments derivatives - axis.endosperm.R_residual = self.respiration_model.RespirationModel.R_endosperm(axis.endosperm.starch, axis.endosperm.mstruct, soil.Tsoil) - starch_derivative = axis.endosperm.calculate_starch_derivative(axis.endosperm.D_starch, axis.endosperm.R_residual) - proteins_derivative = axis.endosperm.calculate_proteins_derivative(axis.endosperm.D_proteins) - y_derivatives[axis.endosperm._i_starch] = starch_derivative - y_derivatives[axis.endosperm._i_proteins] = proteins_derivative - - # Roots - # phloem_contributors.append(axis.roots) - - # compute total transpiration at t_inf + # compute total transpiration at t_inf, for this axis and accumulated at plant scale axis.Total_Transpiration = 0.0 # mmol s-1 for phytomer in axis.phytomers: for organ in (phytomer.chaff, phytomer.peduncle, phytomer.lamina, phytomer.internode, phytomer.sheath): @@ -1296,12 +1352,54 @@ def _calculate_shoot_derivatives(self, t, y): if element is not None and element.green_area > 0: element.Transpiration = element.calculate_Total_Transpiration(element.Tr, element.green_area) axis.Total_Transpiration += (element.Transpiration * element.nb_replications) + plant_Total_Transpiration += axis.Total_Transpiration + plant_mstruct += axis.mstruct + + axis = ms_axis #: owns the (possibly shared) roots/phloem/grains/endosperm from here on + + # Phloem : prebuilt in _initialize_indices_and_sparcity, already merged across every axis of the plant + phloem_contributors = axis.phloem._contributors + + # Endosperm (shared: computed once, on the main stem) + empty_endosperm = True + germinating = False + if axis.endosperm is not None and ((axis.endosperm.starch / axis.endosperm.PARAMETERS.STARCH_MAX) > 0.01 or (axis.endosperm.proteins / axis.endosperm.PARAMETERS.PROTEINS_MAX) > 0.01): + empty_endosperm = False + axis.endosperm.moistening = y[axis.endosperm._i_moistening] # TODO + if axis.endosperm.moistening < 1: + y_derivatives[axis.endosperm._i_moistening] = axis.endosperm.calculate_moistening() + germinating = True + else: + axis.endosperm.starch = y[axis.endosperm._i_starch] + axis.endosperm.proteins = y[axis.endosperm._i_proteins] + + # intermediate variables + T_effect_Vmax = axis.endosperm.calculate_temperature_effect_on_growth(soil.Tsoil) - # Compute the regulating factor of root exports by shoot transpiration - axis.roots.regul_transpiration = axis.roots.calculate_regul_transpiration(axis.Total_Transpiration) + # flows + axis.endosperm.D_starch = axis.endosperm.calculate_D_starch(axis.endosperm.starch, T_effect_Vmax) + axis.endosperm.D_proteins = axis.endosperm.calculate_D_proteins(axis.endosperm.proteins, T_effect_Vmax) - # compute the derivative of each photosynthetic organ element compartment - for phytomer in axis.phytomers: + # compartments derivatives + axis.endosperm.R_residual = self.respiration_model.RespirationModel.R_endosperm(axis.endosperm.starch, axis.endosperm.mstruct, soil.Tsoil) + starch_derivative = axis.endosperm.calculate_starch_derivative(axis.endosperm.D_starch, axis.endosperm.R_residual) + proteins_derivative = axis.endosperm.calculate_proteins_derivative(axis.endosperm.D_proteins) + y_derivatives[axis.endosperm._i_starch] = starch_derivative + y_derivatives[axis.endosperm._i_proteins] = proteins_derivative + + if germinating: + continue + + # Compute the regulating factor of root exports by whole-plant shoot transpiration + axis.roots.regul_transpiration = axis.roots.calculate_regul_transpiration(plant_Total_Transpiration) + + #: mstruct normalizer for exchanges with the shared phloem pool, excluding roots (shoot organs + #: compute their local concentration relative to shoot mass only) -- see NOTE below. + plant_shoot_mstruct = plant_mstruct - axis.roots.mstruct + + # compute the derivative of each photosynthetic organ element compartment, for every axis in turn + for shoot_axis in plant.axes: + for phytomer in shoot_axis.phytomers: # Hidden zone hiddenzone = phytomer.hiddenzone if phytomer.hiddenzone is not None: @@ -1309,7 +1407,6 @@ def _calculate_shoot_derivatives(self, t, y): hiddenzone.fructan = y[hiddenzone._i_fructan] hiddenzone.amino_acids = y[hiddenzone._i_amino_acids] hiddenzone.proteins = y[hiddenzone._i_proteins] - # phloem_contributors.append(hiddenzone) hiddenzone_Loading_Sucrose_contribution = 0 hiddenzone_Loading_Amino_Acids_contribution = 0 @@ -1348,9 +1445,8 @@ def _calculate_shoot_derivatives(self, t, y): hiddenzone_Loading_Amino_Acids_contribution += element.Loading_Amino_Acids else: #: Loading of sucrose and amino acids towards the phloem - # phloem_contributors.append(element) - element.Loading_Sucrose = element.calculate_Loading_Sucrose(element.sucrose, axis.phloem.sucrose, axis.mstruct - axis.roots.mstruct, element.T_effect_conductivity) # NOTE - axis.roots.mstruct to account for phloem massic concentration only - element.Loading_Amino_Acids = element.calculate_Loading_Amino_Acids(element.amino_acids, axis.phloem.amino_acids, axis.mstruct - axis.roots.mstruct, element.T_effect_conductivity) + element.Loading_Sucrose = element.calculate_Loading_Sucrose(element.sucrose, axis.phloem.sucrose, plant_shoot_mstruct, element.T_effect_conductivity) # NOTE - axis.roots.mstruct to account for phloem massic concentration only + element.Loading_Amino_Acids = element.calculate_Loading_Amino_Acids(element.amino_acids, axis.phloem.amino_acids, plant_shoot_mstruct, element.T_effect_conductivity) element.Regul_S_Fructan = element.calculate_Regul_S_Fructan(element.Loading_Sucrose) element.S_Fructan = element.calculate_S_Fructan(element.sucrose, element.Regul_S_Fructan, element.T_effect_Vmax) @@ -1360,14 +1456,20 @@ def _calculate_shoot_derivatives(self, t, y): element.S_Sucrose = element.calculate_S_Sucrose(element.triosesP, element.T_effect_Vmax) element.R_phloem_loading, element.Loading_Sucrose = self.respiration_model.RespirationModel.R_phloem(element.Loading_Sucrose, element.mstruct * element.__class__.PARAMETERS.ALPHA) - element.Nitrates_import = element.calculate_Nitrates_import(axis.roots.Export_Nitrates, element.Transpiration, axis.Total_Transpiration) - element.Amino_Acids_import = element.calculate_Amino_Acids_import(axis.roots.Export_Amino_Acids, element.Transpiration, axis.Total_Transpiration) + element.Nitrates_import = element.calculate_Nitrates_import(axis.roots.Export_Nitrates, element.Transpiration, plant_Total_Transpiration) + element.Amino_Acids_import = element.calculate_Amino_Acids_import(axis.roots.Export_Amino_Acids, element.Transpiration, plant_Total_Transpiration) element.S_Amino_Acids = element.calculate_S_amino_acids(element.nitrates, element.triosesP, element.T_effect_Vmax) element.R_Nnit_red, element.S_Amino_Acids = self.respiration_model.RespirationModel.R_Nnit_red(element.S_Amino_Acids, element.sucrose, element.mstruct * element.__class__.PARAMETERS.ALPHA) element.S_Proteins = element.calculate_S_proteins(element.amino_acids, element.T_effect_Vmax) element.D_Proteins = element.calculate_D_Proteins(element.proteins, element.cytokinins, element.T_effect_Vmax) - element.cytokinins_import = element.calculate_cytokinins_import(axis.roots.Export_cytokinins, element.Transpiration, axis.Total_Transpiration) + #: use the snapshot taken once at the top of run(), not the live axis.roots.Export_cytokinins + #: -- that attribute is itself (re)computed further below in THIS SAME function (from + #: axis.roots.cytokinins/regul_transpiration), so reading it live here made + #: _calculate_shoot_derivatives(t, y) non-deterministic: repeated calls with the + #: identical y (as BDF/num_jac routinely make) would each read whatever the PREVIOUS + #: call had just written, not a value determined purely by (t, y). See run()'s comment. + element.cytokinins_import = element.calculate_cytokinins_import(axis.roots._Export_cytokinins_frozen, element.Transpiration, plant_Total_Transpiration) element.D_cytokinins = element.calculate_D_cytokinins(element.cytokinins, element.T_effect_Vmax) # compartments derivatives @@ -1396,29 +1498,29 @@ def _calculate_shoot_derivatives(self, t, y): if phytomer.hiddenzone is not None: # Unloading of sucrose from phloem - hiddenzone.Unloading_Sucrose = hiddenzone.calculate_Unloading_Sucrose(hiddenzone.sucrose, axis.phloem.sucrose, axis.mstruct - axis.roots.mstruct, axis.T_effect_conductivity) + hiddenzone.Unloading_Sucrose = hiddenzone.calculate_Unloading_Sucrose(hiddenzone.sucrose, axis.phloem.sucrose, plant_shoot_mstruct, shoot_axis.T_effect_conductivity) # Unloading of AA from phloem - hiddenzone.Unloading_Amino_Acids = hiddenzone.calculate_Unloading_Amino_Acids(hiddenzone.amino_acids, axis.phloem.amino_acids, axis.mstruct - axis.roots.mstruct, axis.T_effect_conductivity) + hiddenzone.Unloading_Amino_Acids = hiddenzone.calculate_Unloading_Amino_Acids(hiddenzone.amino_acids, axis.phloem.amino_acids, plant_shoot_mstruct, shoot_axis.T_effect_conductivity) # Fructan synthesis Regul_Sfructanes = hiddenzone.calculate_Regul_S_Fructan(hiddenzone.Unloading_Sucrose) - hiddenzone.S_Fructan = hiddenzone.calculate_S_Fructan(hiddenzone.sucrose, Regul_Sfructanes, axis.T_effect_Vmax) + hiddenzone.S_Fructan = hiddenzone.calculate_S_Fructan(hiddenzone.sucrose, Regul_Sfructanes, shoot_axis.T_effect_Vmax) # Fructan degradation - hiddenzone.D_Fructan = hiddenzone.calculate_D_Fructan(hiddenzone.sucrose, hiddenzone.fructan, axis.T_effect_Vmax) + hiddenzone.D_Fructan = hiddenzone.calculate_D_Fructan(hiddenzone.sucrose, hiddenzone.fructan, shoot_axis.T_effect_Vmax) # Synthesis proteins - hiddenzone.S_Proteins = hiddenzone.calculate_S_proteins(hiddenzone.amino_acids, axis.T_effect_Vmax) + hiddenzone.S_Proteins = hiddenzone.calculate_S_proteins(hiddenzone.amino_acids, shoot_axis.T_effect_Vmax) # Degradation proteins - hiddenzone.D_Proteins = hiddenzone.calculate_D_Proteins(hiddenzone.proteins, axis.T_effect_Vmax) + hiddenzone.D_Proteins = hiddenzone.calculate_D_Proteins(hiddenzone.proteins, shoot_axis.T_effect_Vmax) # Residual respiration hiddenzone.R_residual = self.respiration_model.RespirationModel.R_residual(hiddenzone.sucrose, hiddenzone.mstruct * hiddenzone.__class__.PARAMETERS.ALPHA, hiddenzone.Total_Organic_Nitrogen, - axis.SAM_temperature) + shoot_axis.SAM_temperature) # compute the derivatives of the hidden zone y_derivatives[hiddenzone._i_sucrose] = hiddenzone.calculate_sucrose_derivative(hiddenzone.Unloading_Sucrose, hiddenzone.S_Fructan, @@ -1430,54 +1532,53 @@ def _calculate_shoot_derivatives(self, t, y): y_derivatives[hiddenzone._i_fructan] = hiddenzone.calculate_fructan_derivative(hiddenzone.S_Fructan, hiddenzone.D_Fructan) y_derivatives[hiddenzone._i_proteins] = hiddenzone.calculate_proteins_derivative(hiddenzone.S_Proteins, hiddenzone.D_Proteins) - if axis.grains is not None: - phloem_contributors.append(axis.grains) - # compute the derivative of each compartment of grains - i_structure, i_starch, i_proteins, i_age_from_flowering = axis.grains._i_structure, axis.grains._i_starch, axis.grains._i_proteins, axis.grains._i_age_from_flowering + if axis.grains is not None: + # compute the derivative of each compartment of grains + i_structure, i_starch, i_proteins, i_age_from_flowering = axis.grains._i_structure, axis.grains._i_starch, axis.grains._i_proteins, axis.grains._i_age_from_flowering - axis.grains.structure = y[i_structure] - axis.grains.starch = y[i_starch] - axis.grains.proteins = y[i_proteins] - axis.grains.age_from_flowering = y[i_age_from_flowering] + axis.grains.structure = y[i_structure] + axis.grains.starch = y[i_starch] + axis.grains.proteins = y[i_proteins] + axis.grains.age_from_flowering = y[i_age_from_flowering] - # intermediate variables - T_effect_growth = axis.grains.calculate_temperature_effect_on_growth(axis.SAM_temperature) - axis.grains.structural_dry_mass = axis.grains.calculate_structural_dry_mass(axis.grains.structure) + # intermediate variables + T_effect_growth = axis.grains.calculate_temperature_effect_on_growth(axis.SAM_temperature) + axis.grains.structural_dry_mass = axis.grains.calculate_structural_dry_mass(axis.grains.structure) - # flows - axis.grains.S_grain_structure = axis.grains.calculate_S_grain_structure(axis.grains.structure, axis.phloem.sucrose, axis.mstruct, T_effect_growth) - axis.grains.S_grain_starch = axis.grains.calculate_S_grain_starch(axis.phloem.sucrose, axis.mstruct, axis.T_effect_Vmax) - axis.grains.S_Proteins = axis.grains.calculate_S_proteins(axis.grains.S_grain_structure, axis.grains.S_grain_starch, axis.phloem.amino_acids, axis.phloem.sucrose, - axis.grains.structural_dry_mass) - # compartments derivatives - axis.grains.R_grain_growth_struct, axis.grains.R_grain_growth_starch = self.respiration_model.RespirationModel.R_grain_growth(axis.grains.S_grain_structure, - axis.grains.S_grain_starch, - axis.grains.structural_dry_mass) - structure_derivative = axis.grains.calculate_structure_derivative(axis.grains.S_grain_structure, axis.grains.R_grain_growth_struct) - starch_derivative = axis.grains.calculate_starch_derivative(axis.grains.S_grain_starch, axis.grains.structural_dry_mass, axis.grains.R_grain_growth_starch) - proteins_derivative = axis.grains.calculate_proteins_derivative(axis.grains.S_Proteins) - y_derivatives[i_structure] = structure_derivative - y_derivatives[i_starch] = starch_derivative - y_derivatives[i_proteins] = proteins_derivative - y_derivatives[i_age_from_flowering] += (self.delta_t * T_effect_growth) # TODO: create a function - - # compute the derivative of each compartment of roots # flows - axis.roots.Export_cytokinins = axis.roots.calculate_Export_cytokinins(axis.roots.cytokinins, axis.roots.regul_transpiration) - - if self.cnwgrass_roots: - axis.roots.Unloading_Sucrose = axis.roots.calculate_Unloading_Sucrose(axis.roots.sucrose, axis.phloem.sucrose, axis.mstruct, axis.T_effect_conductivity, axis.nb_leaves) - axis.roots.Unloading_Amino_Acids = axis.roots.calculate_Unloading_Amino_Acids(axis.roots.amino_acids, axis.phloem.amino_acids, axis.phloem.sucrose, axis.roots.Unloading_Sucrose, axis.mstruct, axis.T_effect_conductivity) - axis.roots.Export_Nitrates = axis.roots.calculate_Export_Nitrates(axis.roots.nitrates, axis.roots.regul_transpiration) - axis.roots.Export_Amino_Acids = axis.roots.calculate_Export_Amino_Acids(axis.roots.amino_acids, axis.roots.regul_transpiration) - - # compute the derivative of each compartment of phloem - sucrose_phloem_derivative = axis.phloem.calculate_sucrose_derivative(phloem_contributors) - axis.phloem.Unloading_Sucrose_shoot_organs = sucrose_phloem_derivative + (axis.roots.Unloading_Sucrose * axis.roots.mstruct * axis.roots.__class__.PARAMETERS.ALPHA) - amino_acids_phloem_derivative = axis.phloem.calculate_amino_acids_derivative(phloem_contributors) - axis.phloem.Unloading_Amino_Acids_shoot_organs = amino_acids_phloem_derivative + (axis.roots.Unloading_Amino_Acids * axis.roots.mstruct * axis.roots.__class__.PARAMETERS.ALPHA) - y_derivatives[axis.phloem._i_sucrose] = sucrose_phloem_derivative - y_derivatives[axis.phloem._i_amino_acids] = amino_acids_phloem_derivative + axis.grains.S_grain_structure = axis.grains.calculate_S_grain_structure(axis.grains.structure, axis.phloem.sucrose, plant_mstruct, T_effect_growth) + axis.grains.S_grain_starch = axis.grains.calculate_S_grain_starch(axis.phloem.sucrose, plant_mstruct, axis.T_effect_Vmax) + axis.grains.S_Proteins = axis.grains.calculate_S_proteins(axis.grains.S_grain_structure, axis.grains.S_grain_starch, axis.phloem.amino_acids, axis.phloem.sucrose, + axis.grains.structural_dry_mass) + # compartments derivatives + axis.grains.R_grain_growth_struct, axis.grains.R_grain_growth_starch = self.respiration_model.RespirationModel.R_grain_growth(axis.grains.S_grain_structure, + axis.grains.S_grain_starch, + axis.grains.structural_dry_mass) + structure_derivative = axis.grains.calculate_structure_derivative(axis.grains.S_grain_structure, axis.grains.R_grain_growth_struct) + starch_derivative = axis.grains.calculate_starch_derivative(axis.grains.S_grain_starch, axis.grains.structural_dry_mass, axis.grains.R_grain_growth_starch) + proteins_derivative = axis.grains.calculate_proteins_derivative(axis.grains.S_Proteins) + y_derivatives[i_structure] = structure_derivative + y_derivatives[i_starch] = starch_derivative + y_derivatives[i_proteins] = proteins_derivative + y_derivatives[i_age_from_flowering] += (self.delta_t * T_effect_growth) # TODO: create a function + + # compute the derivative of each compartment of roots + # flows + axis.roots.Export_cytokinins = axis.roots.calculate_Export_cytokinins(axis.roots.cytokinins, axis.roots.regul_transpiration) + + if self.cnwgrass_roots: + axis.roots.Unloading_Sucrose = axis.roots.calculate_Unloading_Sucrose(axis.roots.sucrose, axis.phloem.sucrose, plant_mstruct, axis.T_effect_conductivity, axis.nb_leaves) + axis.roots.Unloading_Amino_Acids = axis.roots.calculate_Unloading_Amino_Acids(axis.roots.amino_acids, axis.phloem.amino_acids, axis.phloem.sucrose, axis.roots.Unloading_Sucrose, plant_mstruct, axis.T_effect_conductivity) + axis.roots.Export_Nitrates = axis.roots.calculate_Export_Nitrates(axis.roots.nitrates, axis.roots.regul_transpiration) + axis.roots.Export_Amino_Acids = axis.roots.calculate_Export_Amino_Acids(axis.roots.amino_acids, axis.roots.regul_transpiration) + + # compute the derivative of each compartment of phloem + sucrose_phloem_derivative = axis.phloem.calculate_sucrose_derivative(phloem_contributors) + axis.phloem.Unloading_Sucrose_shoot_organs = sucrose_phloem_derivative + (axis.roots.Unloading_Sucrose * axis.roots.mstruct * axis.roots.__class__.PARAMETERS.ALPHA) + amino_acids_phloem_derivative = axis.phloem.calculate_amino_acids_derivative(phloem_contributors) + axis.phloem.Unloading_Amino_Acids_shoot_organs = amino_acids_phloem_derivative + (axis.roots.Unloading_Amino_Acids * axis.roots.mstruct * axis.roots.__class__.PARAMETERS.ALPHA) + y_derivatives[axis.phloem._i_sucrose] = sucrose_phloem_derivative + y_derivatives[axis.phloem._i_amino_acids] = amino_acids_phloem_derivative derivatives_logger = logging.getLogger('cnmetabolism.derivatives') if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): @@ -1546,7 +1647,8 @@ def _calculate_root_derivatives(self, t, y): # 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' + offenders = self._describe_nan(y, self.initial_conditions_mapping_roots) + message = 'The solver did not manage to compute a compartment. See the logs. NaN found in y: {}'.format('; '.join(offenders)) logger.exception(message) raise SimulationRunError(message) @@ -1565,6 +1667,14 @@ def _calculate_root_derivatives(self, t, y): axis.T_effect_conductivity = plant.calculate_temperature_effect_on_conductivity(axis.SAM_temperature) axis.T_effect_Vmax = plant.calculate_temperature_effect_on_Vmax(axis.SAM_temperature) + #: axis.roots may be the SAME shared object across several axes of one plant (main stem + + #: explicit tillers, see cnmetabolism_facade._initialize_model): compute its regulatory and + #: derivative equations, and its contribution to the shared soil nitrate pool, ONCE per plant + #: (on the main stem, which owns it) -- not once per axis, which would double (or N-fold, for + #: N axes) the plant's soil N uptake and redundantly recompute the same ODE derivative. + if axis.label != 'MS': + continue + # Roots axis.roots.nitrates = y[self.initial_conditions_mapping_roots[axis.roots]['nitrates']] axis.roots.amino_acids = y[self.initial_conditions_mapping_roots[axis.roots]['amino_acids']] @@ -1575,7 +1685,7 @@ def _calculate_root_derivatives(self, t, y): if not self.external_soil_model: axis.roots.Uptake_Nitrates, axis.roots.HATS_LATS = axis.roots.calculate_Uptake_Nitrates(soil.Conc_Nitrates_Soil, axis.roots.nitrates, axis.roots.sucrose, soil.T_effect_Vmax, soil.SRWC) - soil_contributors.append((axis.roots.Uptake_Nitrates, plant.index)) #: TODO TEMP!!! + soil_contributors.append((axis.roots.Uptake_Nitrates, plant.index)) #: safe: guarded to axis.label == 'MS' above, so this runs once per plant even when roots are shared across tillers else: assert axis.roots is not None axis.roots.R_Nnit_upt = self.respiration_model.RespirationModel.R_Nnit_upt(axis.roots.Uptake_Nitrates, axis.roots.sucrose) diff --git a/src/openalea/cnwgrass/gasexchange/simulation.py b/src/openalea/cnwgrass/gasexchange/simulation.py index 033336f..fa80a16 100644 --- a/src/openalea/cnwgrass/gasexchange/simulation.py +++ b/src/openalea/cnwgrass/gasexchange/simulation.py @@ -104,7 +104,7 @@ def run(self, Ta, ambient_CO2, RH, Ur): the results in :attr:`outputs`. :param float Ta: air temperature at t (degree Celsius) - :param float ambient_CO2: air CO2 at t (µmol mol-1) + :param float ambient_CO2: air CO2 at t (�mol mol-1) :param float RH: relative humidity at t (decimal fraction) :param float Ur: wind speed at the top of the canopy at t (m s-1) """ @@ -116,9 +116,6 @@ def run(self, Ta, ambient_CO2, RH, Ur): axis_id = element_id[:2] organ_label = element_id[3] - axe_label = axis_id[1] - if axe_label != 'MS': # Calculation only for the main stem - continue # In case it is an HiddenElement, we need temperature calculation. # Cases of Visible Element without geometry property (because too small) don't have photosynthesis calculation neither. if element_inputs['height'] is None or np.isnan(element_inputs['height']): @@ -126,11 +123,15 @@ def run(self, Ta, ambient_CO2, RH, Ur): Ts = self.inputs['axes'][axis_id]['SAM_temperature'] else: - Ts = Ta # Initial value of Ts (°C) - if element_inputs['Ci'] is None or np.isnan(element_inputs['Ci']): + Ts = Ta # Initial value of Ts (�C) + #: Ci is stored as exactly 0 (see the surfacic_nitrogen == 0 branch below) whenever the element + #: had no photosynthetic nitrogen at the previous step -- treat that the same as "no previous + #: value" (like None/NaN) rather than feeding Ci=0 into calculate_photosynthesis, which divides + #: by Ci and would raise ZeroDivisionError once nitrogen becomes nonzero again. + if element_inputs['Ci'] is None or np.isnan(element_inputs['Ci']) or element_inputs['Ci'] <= 0: Ci = ambient_CO2 else: - Ci = element_inputs['Ci'] #: previous organ internal CO2 concentration (µmol mol-1) todo Ci = parameters.Ci_init_ratio * ambient_CO2 see with Victoria if we keep this + Ci = element_inputs['Ci'] #: previous organ internal CO2 concentration (�mol mol-1) todo Ci = parameters.Ci_init_ratio * ambient_CO2 see with Victoria if we keep this height_canopy = self.inputs['axes'][axis_id]['height_canopy'] water_potential = element_inputs.get('water_potential', None) diff --git a/src/openalea/cnwgrass/growth/simulation.py b/src/openalea/cnwgrass/growth/simulation.py index 8419c3a..720af0b 100644 --- a/src/openalea/cnwgrass/growth/simulation.py +++ b/src/openalea/cnwgrass/growth/simulation.py @@ -122,289 +122,282 @@ def run(self, postflowering_stages=False): # ---------------------------------------------- for hiddenzone_id, hiddenzone_inputs in sorted(all_hiddenzone_inputs.items()): curr_hiddenzone_outputs = all_hiddenzone_outputs[hiddenzone_id] - axe_label = hiddenzone_id[1] phytomer_id = hiddenzone_id[2] - #: Tillers (we copy corresponding elements of MS) - if axe_label != 'MS': # TODO: temporary or should be an option at least - pass - - #: Main stem + # Initialisation of the exports towards the growing lamina or sheath + delta_leaf_enclosed_mstruct = delta_leaf_enclosed_Nstruct = delta_lamina_mstruct = delta_sheath_mstruct = delta_lamina_Nstruct = delta_sheath_Nstruct = leaf_export_sucrose = \ + delta_internode_Nstruct = leaf_export_amino_acids = leaf_remob_fructan = leaf_export_proteins = internode_export_sucrose = \ + internode_export_amino_acids = internode_remob_fructan = internode_export_proteins = 0. + + # -- Delta Growth internode + if hiddenzone_inputs['internode_pseudo_age'] < self.model.parameters.internode_rapid_growth_t: #: Internode is not yet in rapide growth stage TODO : tester sur une variable "is_ligulated" + # delta mstruct of the internode + ratio_mstruct_DM = self.model.calculate_ratio_mstruct_DM(hiddenzone_inputs['mstruct'], hiddenzone_inputs['sucrose'], hiddenzone_inputs['fructan'], + hiddenzone_inputs['amino_acids'], hiddenzone_inputs['proteins']) + delta_internode_enclosed_mstruct = self.model.calculate_delta_internode_enclosed_mstruct(hiddenzone_inputs['internode_L'], hiddenzone_inputs['delta_internode_L'], ratio_mstruct_DM) + # delta Nstruct of the internode + delta_internode_enclosed_Nstruct = self.model.calculate_delta_Nstruct(delta_internode_enclosed_mstruct) else: - # Initialisation of the exports towards the growing lamina or sheath - delta_leaf_enclosed_mstruct = delta_leaf_enclosed_Nstruct = delta_lamina_mstruct = delta_sheath_mstruct = delta_lamina_Nstruct = delta_sheath_Nstruct = leaf_export_sucrose = \ - delta_internode_Nstruct = leaf_export_amino_acids = leaf_remob_fructan = leaf_export_proteins = internode_export_sucrose = \ - internode_export_amino_acids = internode_remob_fructan = internode_export_proteins = 0. - - # -- Delta Growth internode - if hiddenzone_inputs['internode_pseudo_age'] < self.model.parameters.internode_rapid_growth_t: #: Internode is not yet in rapide growth stage TODO : tester sur une variable "is_ligulated" - # delta mstruct of the internode - ratio_mstruct_DM = self.model.calculate_ratio_mstruct_DM(hiddenzone_inputs['mstruct'], hiddenzone_inputs['sucrose'], hiddenzone_inputs['fructan'], - hiddenzone_inputs['amino_acids'], hiddenzone_inputs['proteins']) - delta_internode_enclosed_mstruct = self.model.calculate_delta_internode_enclosed_mstruct(hiddenzone_inputs['internode_L'], hiddenzone_inputs['delta_internode_L'], ratio_mstruct_DM) - # delta Nstruct of the internode - delta_internode_enclosed_Nstruct = self.model.calculate_delta_Nstruct(delta_internode_enclosed_mstruct) + # delta mstruct of the enclosed internode + delta_internode_enclosed_mstruct = self.model.calculate_delta_internode_enclosed_mstruct_postL(hiddenzone_inputs['delta_internode_pseudo_age'], + hiddenzone_inputs['internode_pseudo_age'], + hiddenzone_inputs['internode_L'], + hiddenzone_inputs['internode_distance_to_emerge'], + hiddenzone_inputs['internode_Lmax'], + hiddenzone_inputs['LSIW'], + hiddenzone_inputs['internode_enclosed_mstruct']) + # delta Nstruct of the enclosed internode + delta_internode_enclosed_Nstruct = self.model.calculate_delta_Nstruct(delta_internode_enclosed_mstruct) + + if hiddenzone_inputs['internode_is_visible']: #: Internode is visible + visible_internode_id = hiddenzone_id + tuple(['internode', 'StemElement']) + curr_visible_internode_inputs = all_elements_inputs[visible_internode_id] + curr_visible_internode_outputs = all_elements_outputs[visible_internode_id] + # Delta mstruct of the emerged internode + delta_internode_mstruct = self.model.calculate_delta_emerged_tissue_mstruct(hiddenzone_inputs['LSIW'], curr_visible_internode_inputs['mstruct'], curr_visible_internode_inputs['length']) + # Delta Nstruct of the emerged internode + delta_internode_Nstruct = self.model.calculate_delta_Nstruct(delta_internode_mstruct) + # Export of sucrose from hiddenzone towards emerged internode + internode_export_sucrose = self.model.calculate_export(delta_internode_mstruct, hiddenzone_inputs['sucrose'], hiddenzone_inputs['mstruct']) + # Export of amino acids from hiddenzone towards emerged internode + internode_export_amino_acids = self.model.calculate_export(delta_internode_mstruct, hiddenzone_inputs['amino_acids'], hiddenzone_inputs['mstruct']) + internode_remob_fructan = self.model.calculate_export(delta_internode_mstruct, hiddenzone_inputs['fructan'], hiddenzone_inputs['mstruct']) + internode_export_proteins = self.model.calculate_export(delta_internode_mstruct, hiddenzone_inputs['proteins'], hiddenzone_inputs['mstruct']) + + # Update of internode outputs + curr_visible_internode_outputs['mstruct'] += delta_internode_mstruct + curr_visible_internode_outputs['max_mstruct'] = curr_visible_internode_outputs['mstruct'] + curr_visible_internode_outputs['Nstruct'] += delta_internode_Nstruct + curr_visible_internode_outputs['sucrose'] += internode_export_sucrose + internode_remob_fructan + curr_visible_internode_outputs['amino_acids'] += internode_export_amino_acids + curr_visible_internode_outputs['proteins'] += internode_export_proteins + self.outputs['elements'][visible_internode_id] = curr_visible_internode_outputs + + # -- Delta Growth leaf + if not hiddenzone_inputs['leaf_is_emerged']: #: Leaf is not emerged + # delta mstruct of the hidden leaf + ratio_mstruct_DM = self.model.calculate_ratio_mstruct_DM(hiddenzone_inputs['mstruct'], hiddenzone_inputs['sucrose'], hiddenzone_inputs['fructan'], + hiddenzone_inputs['amino_acids'], hiddenzone_inputs['proteins']) + init_leaf_L = hiddenzone_inputs.get('init_leaf_L') # Set at None if hydraulics is False + delta_leaf_enclosed_mstruct = self.model.calculate_delta_leaf_enclosed_mstruct(hiddenzone_inputs['leaf_L'], hiddenzone_inputs['delta_leaf_L'], ratio_mstruct_DM, + init_leaf_L, hiddenzone_inputs['leaf_pseudo_age']) + + # delta Nstruct of the hidden leaf + delta_leaf_enclosed_Nstruct = self.model.calculate_delta_Nstruct(delta_leaf_enclosed_mstruct) + elif hiddenzone_inputs['leaf_is_growing']: #: Leaf has emerged and growing + delta_leaf_enclosed_mstruct = self.model.calculate_delta_leaf_enclosed_mstruct_postE(hiddenzone_inputs['delta_leaf_pseudo_age'], + hiddenzone_inputs['leaf_pseudo_age'], + hiddenzone_inputs['leaf_pseudostem_length'], + hiddenzone_inputs['leaf_enclosed_mstruct'], + hiddenzone_inputs['LSSW'], + hiddenzone_inputs['sucrose'], + hiddenzone_inputs['mstruct']) + # delta Nstruct of the enclosed en leaf + delta_leaf_enclosed_Nstruct = self.model.calculate_delta_Nstruct(delta_leaf_enclosed_mstruct) + + # leaf has emerged and still growing + visible_lamina_id = hiddenzone_id + tuple(['blade', 'LeafElement1']) + #: Lamina is growing + if visible_lamina_id in all_elements_inputs and all_elements_inputs[visible_lamina_id]['is_growing']: + curr_visible_lamina_inputs = all_elements_inputs[visible_lamina_id] + curr_visible_lamina_outputs = all_elements_outputs[visible_lamina_id] + # Delta mstruct of the emerged lamina + delta_lamina_mstruct = self.model.calculate_delta_emerged_tissue_mstruct(hiddenzone_inputs['SSLW'], curr_visible_lamina_inputs['mstruct'], curr_visible_lamina_inputs['green_area']) + # Delta Nstruct of the emerged lamina + delta_lamina_Nstruct = self.model.calculate_delta_Nstruct(delta_lamina_mstruct) + # Export of metabolite from hiddenzone towards emerged lamina + leaf_export_sucrose = self.model.calculate_export(delta_lamina_mstruct, hiddenzone_inputs['sucrose'], hiddenzone_inputs['mstruct']) + leaf_export_amino_acids = self.model.calculate_export(delta_lamina_mstruct, hiddenzone_inputs['amino_acids'], hiddenzone_inputs['mstruct']) + leaf_remob_fructan = self.model.calculate_export(delta_lamina_mstruct, hiddenzone_inputs['fructan'], hiddenzone_inputs['mstruct']) + leaf_export_proteins = self.model.calculate_export(delta_lamina_mstruct, hiddenzone_inputs['proteins'], hiddenzone_inputs['mstruct']) + # Cytokinins in the newly visible mstruct + addition_cytokinins = self.model.calculate_init_cytokinins_emerged_tissue(delta_lamina_mstruct) + + # Update of lamina outputs + curr_visible_lamina_outputs['mstruct'] += delta_lamina_mstruct + curr_visible_lamina_outputs['max_mstruct'] = curr_visible_lamina_outputs['mstruct'] + curr_visible_lamina_outputs['Nstruct'] += delta_lamina_Nstruct + curr_visible_lamina_outputs['sucrose'] += leaf_export_sucrose + leaf_remob_fructan + curr_visible_lamina_outputs['amino_acids'] += leaf_export_amino_acids + curr_visible_lamina_outputs['proteins'] += leaf_export_proteins + curr_visible_lamina_outputs['cytokinins'] += addition_cytokinins + + self.outputs['elements'][visible_lamina_id] = curr_visible_lamina_outputs + + else: #: Mature lamina, growing sheath + if not self.hydraulics: + # The hidden part of the sheath is only updated once, at the end of leaf elongation, by remobilisation from the hiddenzone + visible_sheath_id = hiddenzone_id + tuple(['sheath', 'StemElement']) + curr_visible_sheath_inputs = all_elements_inputs[visible_sheath_id] + curr_visible_sheath_outputs = all_elements_outputs[visible_sheath_id] + # Delta mstruct of the emerged sheath + delta_sheath_mstruct = self.model.calculate_delta_emerged_tissue_mstruct(hiddenzone_inputs['LSSW'], curr_visible_sheath_inputs['mstruct'], curr_visible_sheath_inputs['length']) + # Delta Nstruct of the emerged sheath + delta_sheath_Nstruct = self.model.calculate_delta_Nstruct(delta_sheath_mstruct) + # Export of metabolite from hiddenzone towards emerged sheath + leaf_export_sucrose = self.model.calculate_export(delta_sheath_mstruct, hiddenzone_inputs['sucrose'], hiddenzone_inputs['mstruct']) + leaf_export_amino_acids = self.model.calculate_export(delta_sheath_mstruct, hiddenzone_inputs['amino_acids'], hiddenzone_inputs['mstruct']) + leaf_remob_fructan = self.model.calculate_export(delta_sheath_mstruct, hiddenzone_inputs['fructan'], hiddenzone_inputs['mstruct']) + leaf_export_proteins = self.model.calculate_export(delta_sheath_mstruct, hiddenzone_inputs['proteins'], hiddenzone_inputs['mstruct']) + addition_cytokinins = self.model.calculate_init_cytokinins_emerged_tissue(delta_sheath_mstruct) + + # Update of sheath outputs + curr_visible_sheath_outputs['mstruct'] += delta_sheath_mstruct + curr_visible_sheath_outputs['max_mstruct'] = curr_visible_sheath_outputs['mstruct'] + curr_visible_sheath_outputs['Nstruct'] += delta_sheath_Nstruct + curr_visible_sheath_outputs['sucrose'] += leaf_export_sucrose + leaf_remob_fructan + curr_visible_sheath_outputs['amino_acids'] += leaf_export_amino_acids + curr_visible_sheath_outputs['proteins'] += leaf_export_proteins + curr_visible_sheath_outputs['cytokinins'] += addition_cytokinins + self.outputs['elements'][visible_sheath_id] = curr_visible_sheath_outputs + + # -- CN consumption due to mstruct/Nstruct growth of the enclosed leaf and of the internode + curr_hiddenzone_outputs['AA_consumption_mstruct'] = self.model.calculate_s_Nstruct_amino_acids((delta_leaf_enclosed_Nstruct + delta_internode_enclosed_Nstruct), + delta_lamina_Nstruct, + delta_sheath_Nstruct, + delta_internode_Nstruct) #: Consumption of amino acids due to mstruct growth (�mol N) + curr_hiddenzone_outputs['sucrose_consumption_mstruct'] = self.model.calculate_s_mstruct_sucrose((delta_leaf_enclosed_mstruct + delta_internode_enclosed_mstruct), + delta_lamina_mstruct, + delta_sheath_mstruct, + curr_hiddenzone_outputs[ + 'AA_consumption_mstruct']) #: Consumption of sucrose due to mstruct growth (�mol C) + curr_hiddenzone_outputs['Respi_growth'] = RespirationModel.R_growth(curr_hiddenzone_outputs['sucrose_consumption_mstruct']) #: Respiration growth (�mol C) + + # -- Update of hiddenzone outputs + curr_hiddenzone_outputs['leaf_enclosed_mstruct'] += delta_leaf_enclosed_mstruct + curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] += delta_leaf_enclosed_Nstruct + curr_hiddenzone_outputs['internode_enclosed_mstruct'] += delta_internode_enclosed_mstruct + curr_hiddenzone_outputs['internode_enclosed_Nstruct'] += delta_internode_enclosed_Nstruct + curr_hiddenzone_outputs['mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] + curr_hiddenzone_outputs['internode_enclosed_mstruct'] + curr_hiddenzone_outputs['Nstruct'] = curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] + curr_hiddenzone_outputs['internode_enclosed_Nstruct'] + curr_hiddenzone_outputs['sucrose'] -= ( + curr_hiddenzone_outputs['sucrose_consumption_mstruct'] + curr_hiddenzone_outputs['Respi_growth'] + leaf_export_sucrose + internode_export_sucrose) + curr_hiddenzone_outputs['fructan'] -= (leaf_remob_fructan + internode_remob_fructan) + curr_hiddenzone_outputs['amino_acids'] -= (curr_hiddenzone_outputs['AA_consumption_mstruct'] + leaf_export_amino_acids + internode_export_amino_acids) + curr_hiddenzone_outputs['proteins'] -= (leaf_export_proteins + internode_export_proteins) + self.outputs['hiddenzone'][hiddenzone_id] = curr_hiddenzone_outputs + + # -- Remobilisation at the end of leaf elongation + if hiddenzone_inputs['leaf_is_remobilizing']: + if phytomer_id == 0: + share_leaf = share_hidden_sheath = 1 else: - # delta mstruct of the enclosed internode - delta_internode_enclosed_mstruct = self.model.calculate_delta_internode_enclosed_mstruct_postL(hiddenzone_inputs['delta_internode_pseudo_age'], - hiddenzone_inputs['internode_pseudo_age'], - hiddenzone_inputs['internode_L'], - hiddenzone_inputs['internode_distance_to_emerge'], - hiddenzone_inputs['internode_Lmax'], - hiddenzone_inputs['LSIW'], - hiddenzone_inputs['internode_enclosed_mstruct']) - # delta Nstruct of the enclosed internode - delta_internode_enclosed_Nstruct = self.model.calculate_delta_Nstruct(delta_internode_enclosed_mstruct) - - if hiddenzone_inputs['internode_is_visible']: #: Internode is visible - visible_internode_id = hiddenzone_id + tuple(['internode', 'StemElement']) - curr_visible_internode_inputs = all_elements_inputs[visible_internode_id] - curr_visible_internode_outputs = all_elements_outputs[visible_internode_id] - # Delta mstruct of the emerged internode - delta_internode_mstruct = self.model.calculate_delta_emerged_tissue_mstruct(hiddenzone_inputs['LSIW'], curr_visible_internode_inputs['mstruct'], curr_visible_internode_inputs['length']) - # Delta Nstruct of the emerged internode - delta_internode_Nstruct = self.model.calculate_delta_Nstruct(delta_internode_mstruct) - # Export of sucrose from hiddenzone towards emerged internode - internode_export_sucrose = self.model.calculate_export(delta_internode_mstruct, hiddenzone_inputs['sucrose'], hiddenzone_inputs['mstruct']) - # Export of amino acids from hiddenzone towards emerged internode - internode_export_amino_acids = self.model.calculate_export(delta_internode_mstruct, hiddenzone_inputs['amino_acids'], hiddenzone_inputs['mstruct']) - internode_remob_fructan = self.model.calculate_export(delta_internode_mstruct, hiddenzone_inputs['fructan'], hiddenzone_inputs['mstruct']) - internode_export_proteins = self.model.calculate_export(delta_internode_mstruct, hiddenzone_inputs['proteins'], hiddenzone_inputs['mstruct']) - - # Update of internode outputs - curr_visible_internode_outputs['mstruct'] += delta_internode_mstruct - curr_visible_internode_outputs['max_mstruct'] = curr_visible_internode_outputs['mstruct'] - curr_visible_internode_outputs['Nstruct'] += delta_internode_Nstruct - curr_visible_internode_outputs['sucrose'] += internode_export_sucrose + internode_remob_fructan - curr_visible_internode_outputs['amino_acids'] += internode_export_amino_acids - curr_visible_internode_outputs['proteins'] += internode_export_proteins - self.outputs['elements'][visible_internode_id] = curr_visible_internode_outputs - - # -- Delta Growth leaf - if not hiddenzone_inputs['leaf_is_emerged']: #: Leaf is not emerged - # delta mstruct of the hidden leaf - ratio_mstruct_DM = self.model.calculate_ratio_mstruct_DM(hiddenzone_inputs['mstruct'], hiddenzone_inputs['sucrose'], hiddenzone_inputs['fructan'], - hiddenzone_inputs['amino_acids'], hiddenzone_inputs['proteins']) - init_leaf_L = hiddenzone_inputs.get('init_leaf_L') # Set at None if hydraulics is False - delta_leaf_enclosed_mstruct = self.model.calculate_delta_leaf_enclosed_mstruct(hiddenzone_inputs['leaf_L'], hiddenzone_inputs['delta_leaf_L'], ratio_mstruct_DM, - init_leaf_L, hiddenzone_inputs['leaf_pseudo_age']) - - # delta Nstruct of the hidden leaf - delta_leaf_enclosed_Nstruct = self.model.calculate_delta_Nstruct(delta_leaf_enclosed_mstruct) - elif hiddenzone_inputs['leaf_is_growing']: #: Leaf has emerged and growing - delta_leaf_enclosed_mstruct = self.model.calculate_delta_leaf_enclosed_mstruct_postE(hiddenzone_inputs['delta_leaf_pseudo_age'], - hiddenzone_inputs['leaf_pseudo_age'], - hiddenzone_inputs['leaf_pseudostem_length'], - hiddenzone_inputs['leaf_enclosed_mstruct'], - hiddenzone_inputs['LSSW'], - hiddenzone_inputs['sucrose'], - hiddenzone_inputs['mstruct']) - # delta Nstruct of the enclosed en leaf - delta_leaf_enclosed_Nstruct = self.model.calculate_delta_Nstruct(delta_leaf_enclosed_mstruct) - - # leaf has emerged and still growing - visible_lamina_id = hiddenzone_id + tuple(['blade', 'LeafElement1']) - #: Lamina is growing - if visible_lamina_id in all_elements_inputs and all_elements_inputs[visible_lamina_id]['is_growing']: - curr_visible_lamina_inputs = all_elements_inputs[visible_lamina_id] - curr_visible_lamina_outputs = all_elements_outputs[visible_lamina_id] - # Delta mstruct of the emerged lamina - delta_lamina_mstruct = self.model.calculate_delta_emerged_tissue_mstruct(hiddenzone_inputs['SSLW'], curr_visible_lamina_inputs['mstruct'], curr_visible_lamina_inputs['green_area']) - # Delta Nstruct of the emerged lamina - delta_lamina_Nstruct = self.model.calculate_delta_Nstruct(delta_lamina_mstruct) - # Export of metabolite from hiddenzone towards emerged lamina - leaf_export_sucrose = self.model.calculate_export(delta_lamina_mstruct, hiddenzone_inputs['sucrose'], hiddenzone_inputs['mstruct']) - leaf_export_amino_acids = self.model.calculate_export(delta_lamina_mstruct, hiddenzone_inputs['amino_acids'], hiddenzone_inputs['mstruct']) - leaf_remob_fructan = self.model.calculate_export(delta_lamina_mstruct, hiddenzone_inputs['fructan'], hiddenzone_inputs['mstruct']) - leaf_export_proteins = self.model.calculate_export(delta_lamina_mstruct, hiddenzone_inputs['proteins'], hiddenzone_inputs['mstruct']) - # Cytokinins in the newly visible mstruct - addition_cytokinins = self.model.calculate_init_cytokinins_emerged_tissue(delta_lamina_mstruct) - - # Update of lamina outputs - curr_visible_lamina_outputs['mstruct'] += delta_lamina_mstruct - curr_visible_lamina_outputs['max_mstruct'] = curr_visible_lamina_outputs['mstruct'] - curr_visible_lamina_outputs['Nstruct'] += delta_lamina_Nstruct - curr_visible_lamina_outputs['sucrose'] += leaf_export_sucrose + leaf_remob_fructan - curr_visible_lamina_outputs['amino_acids'] += leaf_export_amino_acids - curr_visible_lamina_outputs['proteins'] += leaf_export_proteins - curr_visible_lamina_outputs['cytokinins'] += addition_cytokinins - - self.outputs['elements'][visible_lamina_id] = curr_visible_lamina_outputs - - else: #: Mature lamina, growing sheath - if not self.hydraulics: - # The hidden part of the sheath is only updated once, at the end of leaf elongation, by remobilisation from the hiddenzone - visible_sheath_id = hiddenzone_id + tuple(['sheath', 'StemElement']) - curr_visible_sheath_inputs = all_elements_inputs[visible_sheath_id] - curr_visible_sheath_outputs = all_elements_outputs[visible_sheath_id] - # Delta mstruct of the emerged sheath - delta_sheath_mstruct = self.model.calculate_delta_emerged_tissue_mstruct(hiddenzone_inputs['LSSW'], curr_visible_sheath_inputs['mstruct'], curr_visible_sheath_inputs['length']) - # Delta Nstruct of the emerged sheath - delta_sheath_Nstruct = self.model.calculate_delta_Nstruct(delta_sheath_mstruct) - # Export of metabolite from hiddenzone towards emerged sheath - leaf_export_sucrose = self.model.calculate_export(delta_sheath_mstruct, hiddenzone_inputs['sucrose'], hiddenzone_inputs['mstruct']) - leaf_export_amino_acids = self.model.calculate_export(delta_sheath_mstruct, hiddenzone_inputs['amino_acids'], hiddenzone_inputs['mstruct']) - leaf_remob_fructan = self.model.calculate_export(delta_sheath_mstruct, hiddenzone_inputs['fructan'], hiddenzone_inputs['mstruct']) - leaf_export_proteins = self.model.calculate_export(delta_sheath_mstruct, hiddenzone_inputs['proteins'], hiddenzone_inputs['mstruct']) - addition_cytokinins = self.model.calculate_init_cytokinins_emerged_tissue(delta_sheath_mstruct) - - # Update of sheath outputs - curr_visible_sheath_outputs['mstruct'] += delta_sheath_mstruct - curr_visible_sheath_outputs['max_mstruct'] = curr_visible_sheath_outputs['mstruct'] - curr_visible_sheath_outputs['Nstruct'] += delta_sheath_Nstruct - curr_visible_sheath_outputs['sucrose'] += leaf_export_sucrose + leaf_remob_fructan - curr_visible_sheath_outputs['amino_acids'] += leaf_export_amino_acids - curr_visible_sheath_outputs['proteins'] += leaf_export_proteins - curr_visible_sheath_outputs['cytokinins'] += addition_cytokinins - self.outputs['elements'][visible_sheath_id] = curr_visible_sheath_outputs - - # -- CN consumption due to mstruct/Nstruct growth of the enclosed leaf and of the internode - curr_hiddenzone_outputs['AA_consumption_mstruct'] = self.model.calculate_s_Nstruct_amino_acids((delta_leaf_enclosed_Nstruct + delta_internode_enclosed_Nstruct), - delta_lamina_Nstruct, - delta_sheath_Nstruct, - delta_internode_Nstruct) #: Consumption of amino acids due to mstruct growth (�mol N) - curr_hiddenzone_outputs['sucrose_consumption_mstruct'] = self.model.calculate_s_mstruct_sucrose((delta_leaf_enclosed_mstruct + delta_internode_enclosed_mstruct), - delta_lamina_mstruct, - delta_sheath_mstruct, - curr_hiddenzone_outputs[ - 'AA_consumption_mstruct']) #: Consumption of sucrose due to mstruct growth (�mol C) - curr_hiddenzone_outputs['Respi_growth'] = RespirationModel.R_growth(curr_hiddenzone_outputs['sucrose_consumption_mstruct']) #: Respiration growth (�mol C) - - # -- Update of hiddenzone outputs - curr_hiddenzone_outputs['leaf_enclosed_mstruct'] += delta_leaf_enclosed_mstruct - curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] += delta_leaf_enclosed_Nstruct - curr_hiddenzone_outputs['internode_enclosed_mstruct'] += delta_internode_enclosed_mstruct - curr_hiddenzone_outputs['internode_enclosed_Nstruct'] += delta_internode_enclosed_Nstruct - curr_hiddenzone_outputs['mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] + curr_hiddenzone_outputs['internode_enclosed_mstruct'] - curr_hiddenzone_outputs['Nstruct'] = curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] + curr_hiddenzone_outputs['internode_enclosed_Nstruct'] - curr_hiddenzone_outputs['sucrose'] -= ( - curr_hiddenzone_outputs['sucrose_consumption_mstruct'] + curr_hiddenzone_outputs['Respi_growth'] + leaf_export_sucrose + internode_export_sucrose) - curr_hiddenzone_outputs['fructan'] -= (leaf_remob_fructan + internode_remob_fructan) - curr_hiddenzone_outputs['amino_acids'] -= (curr_hiddenzone_outputs['AA_consumption_mstruct'] + leaf_export_amino_acids + internode_export_amino_acids) - curr_hiddenzone_outputs['proteins'] -= (leaf_export_proteins + internode_export_proteins) - self.outputs['hiddenzone'][hiddenzone_id] = curr_hiddenzone_outputs + share_leaf = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] / curr_hiddenzone_outputs['mstruct'] - # -- Remobilisation at the end of leaf elongation - if hiddenzone_inputs['leaf_is_remobilizing']: - if phytomer_id == 0: - share_leaf = share_hidden_sheath = 1 - else: - share_leaf = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] / curr_hiddenzone_outputs['mstruct'] - - if not self.hydraulics: - # Case when the hiddenzone contains a hidden part of lamina - if hiddenzone_inputs['leaf_pseudostem_length'] > hiddenzone_inputs['sheath_Lmax']: - hidden_sheath_mstruct = self.model.calculate_sheath_mstruct(hiddenzone_inputs['sheath_Lmax'], hiddenzone_inputs['LSSW']) - share_hidden_sheath = hidden_sheath_mstruct / curr_hiddenzone_outputs['leaf_enclosed_mstruct'] - else: - share_hidden_sheath = 1 + if not self.hydraulics: + # Case when the hiddenzone contains a hidden part of lamina + if hiddenzone_inputs['leaf_pseudostem_length'] > hiddenzone_inputs['sheath_Lmax']: + hidden_sheath_mstruct = self.model.calculate_sheath_mstruct(hiddenzone_inputs['sheath_Lmax'], hiddenzone_inputs['LSSW']) + share_hidden_sheath = hidden_sheath_mstruct / curr_hiddenzone_outputs['leaf_enclosed_mstruct'] else: share_hidden_sheath = 1 - - # Add to hidden part of the sheath delta_leaf_enclosed_mstruct - hidden_sheath_id = hiddenzone_id + tuple(['sheath', 'HiddenElement']) - if hidden_sheath_id not in self.outputs['elements'].keys(): + else: + share_hidden_sheath = 1 + + # Add to hidden part of the sheath delta_leaf_enclosed_mstruct + hidden_sheath_id = hiddenzone_id + tuple(['sheath', 'HiddenElement']) + if hidden_sheath_id not in self.outputs['elements'].keys(): + new_sheath_outputs = self.organ_init.__dict__.copy() + self.outputs['elements'][hidden_sheath_id] = new_sheath_outputs + curr_hidden_sheath_outputs = self.outputs['elements'][hidden_sheath_id] + curr_hidden_sheath_outputs['mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] * share_hidden_sheath + curr_hidden_sheath_outputs['max_mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] * share_hidden_sheath + curr_hidden_sheath_outputs['Nstruct'] = curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] * share_hidden_sheath + curr_hidden_sheath_outputs['sucrose'] = curr_hiddenzone_outputs['sucrose'] * share_leaf * share_hidden_sheath + curr_hidden_sheath_outputs['amino_acids'] = curr_hiddenzone_outputs['amino_acids'] * share_leaf * share_hidden_sheath + curr_hidden_sheath_outputs['fructan'] = curr_hiddenzone_outputs['fructan'] * share_leaf * share_hidden_sheath + curr_hidden_sheath_outputs['proteins'] = curr_hiddenzone_outputs['proteins'] * share_leaf * share_hidden_sheath + curr_hidden_sheath_outputs['cytokinins'] = self.model.calculate_init_cytokinins_emerged_tissue(curr_hidden_sheath_outputs['mstruct']) + self.outputs['elements'][hidden_sheath_id] = curr_hidden_sheath_outputs + + if self.hydraulics: + # Add to visible part of the sheath + visible_lamina_id = hiddenzone_id + tuple(['blade', 'LeafElement1']) + curr_visible_leaf_inputs = all_elements_inputs[visible_lamina_id] + visible_sheath_id = hiddenzone_id + tuple(['sheath', 'StemElement']) + curr_visible_sheath_inputs = self.inputs['elements'][visible_sheath_id] + curr_visible_sheath_outputs = self.outputs['elements'][visible_sheath_id] + if visible_sheath_id not in self.outputs['elements'].keys(): new_sheath_outputs = self.organ_init.__dict__.copy() - self.outputs['elements'][hidden_sheath_id] = new_sheath_outputs - curr_hidden_sheath_outputs = self.outputs['elements'][hidden_sheath_id] - curr_hidden_sheath_outputs['mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] * share_hidden_sheath - curr_hidden_sheath_outputs['max_mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] * share_hidden_sheath - curr_hidden_sheath_outputs['Nstruct'] = curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] * share_hidden_sheath - curr_hidden_sheath_outputs['sucrose'] = curr_hiddenzone_outputs['sucrose'] * share_leaf * share_hidden_sheath - curr_hidden_sheath_outputs['amino_acids'] = curr_hiddenzone_outputs['amino_acids'] * share_leaf * share_hidden_sheath - curr_hidden_sheath_outputs['fructan'] = curr_hiddenzone_outputs['fructan'] * share_leaf * share_hidden_sheath - curr_hidden_sheath_outputs['proteins'] = curr_hiddenzone_outputs['proteins'] * share_leaf * share_hidden_sheath - curr_hidden_sheath_outputs['cytokinins'] = self.model.calculate_init_cytokinins_emerged_tissue(curr_hidden_sheath_outputs['mstruct']) - self.outputs['elements'][hidden_sheath_id] = curr_hidden_sheath_outputs - - if self.hydraulics: - # Add to visible part of the sheath - visible_lamina_id = hiddenzone_id + tuple(['blade', 'LeafElement1']) - curr_visible_leaf_inputs = all_elements_inputs[visible_lamina_id] - visible_sheath_id = hiddenzone_id + tuple(['sheath', 'StemElement']) - curr_visible_sheath_inputs = self.inputs['elements'][visible_sheath_id] - curr_visible_sheath_outputs = self.outputs['elements'][visible_sheath_id] - if visible_sheath_id not in self.outputs['elements'].keys(): - new_sheath_outputs = self.organ_init.__dict__.copy() - self.outputs['elements'][visible_sheath_id] = new_sheath_outputs - - share_visible_leaf = curr_visible_sheath_inputs['length'] / curr_visible_leaf_inputs['length'] - - curr_visible_sheath_outputs['mstruct'] = curr_visible_leaf_inputs['mstruct'] * share_visible_leaf - curr_visible_sheath_outputs['max_mstruct'] = curr_visible_leaf_inputs['mstruct'] * share_visible_leaf - curr_visible_sheath_outputs['Nstruct'] = curr_visible_leaf_inputs['Nstruct'] * share_visible_leaf - curr_visible_sheath_outputs['sucrose'] = curr_visible_leaf_inputs['sucrose'] * share_visible_leaf - curr_visible_sheath_outputs['amino_acids'] = curr_visible_leaf_inputs['amino_acids'] * share_visible_leaf - curr_visible_sheath_outputs['fructan'] = curr_visible_leaf_inputs['fructan'] * share_visible_leaf - curr_visible_sheath_outputs['proteins'] = curr_visible_leaf_inputs['proteins'] * share_visible_leaf - curr_visible_sheath_outputs['cytokinins'] = self.model.calculate_init_cytokinins_emerged_tissue(curr_visible_sheath_outputs['mstruct']) * share_visible_leaf - self.outputs['elements'][visible_sheath_id] = curr_visible_sheath_outputs + self.outputs['elements'][visible_sheath_id] = new_sheath_outputs + + share_visible_leaf = curr_visible_sheath_inputs['length'] / curr_visible_leaf_inputs['length'] + + curr_visible_sheath_outputs['mstruct'] = curr_visible_leaf_inputs['mstruct'] * share_visible_leaf + curr_visible_sheath_outputs['max_mstruct'] = curr_visible_leaf_inputs['mstruct'] * share_visible_leaf + curr_visible_sheath_outputs['Nstruct'] = curr_visible_leaf_inputs['Nstruct'] * share_visible_leaf + curr_visible_sheath_outputs['sucrose'] = curr_visible_leaf_inputs['sucrose'] * share_visible_leaf + curr_visible_sheath_outputs['amino_acids'] = curr_visible_leaf_inputs['amino_acids'] * share_visible_leaf + curr_visible_sheath_outputs['fructan'] = curr_visible_leaf_inputs['fructan'] * share_visible_leaf + curr_visible_sheath_outputs['proteins'] = curr_visible_leaf_inputs['proteins'] * share_visible_leaf + curr_visible_sheath_outputs['cytokinins'] = self.model.calculate_init_cytokinins_emerged_tissue(curr_visible_sheath_outputs['mstruct']) * share_visible_leaf + self.outputs['elements'][visible_sheath_id] = curr_visible_sheath_outputs + + # Remove to visible leaf + curr_visible_leaf_outputs = all_elements_outputs[visible_lamina_id] + curr_visible_leaf_outputs['mstruct'] -= curr_visible_sheath_outputs['mstruct'] + curr_visible_leaf_outputs['Nstruct'] -= curr_visible_sheath_outputs['Nstruct'] + curr_visible_leaf_outputs['sucrose'] -= curr_visible_sheath_outputs['sucrose'] + curr_visible_leaf_outputs['amino_acids'] -= curr_visible_sheath_outputs['amino_acids'] + curr_visible_leaf_outputs['fructan'] -= curr_visible_sheath_outputs['fructan'] + curr_visible_leaf_outputs['proteins'] -= curr_visible_sheath_outputs['proteins'] + self.outputs['elements'][visible_lamina_id] = curr_visible_leaf_outputs - # Remove to visible leaf - curr_visible_leaf_outputs = all_elements_outputs[visible_lamina_id] - curr_visible_leaf_outputs['mstruct'] -= curr_visible_sheath_outputs['mstruct'] - curr_visible_leaf_outputs['Nstruct'] -= curr_visible_sheath_outputs['Nstruct'] - curr_visible_leaf_outputs['sucrose'] -= curr_visible_sheath_outputs['sucrose'] - curr_visible_leaf_outputs['amino_acids'] -= curr_visible_sheath_outputs['amino_acids'] - curr_visible_leaf_outputs['fructan'] -= curr_visible_sheath_outputs['fructan'] - curr_visible_leaf_outputs['proteins'] -= curr_visible_sheath_outputs['proteins'] - self.outputs['elements'][visible_lamina_id] = curr_visible_leaf_outputs + else: + # Add to hidden part of the lamina, if any + if share_hidden_sheath < 1: + hidden_lamina_id = hiddenzone_id + tuple(['blade', 'HiddenElement']) + curr_hidden_lamina_outputs = self.outputs['elements'][hidden_lamina_id] + curr_hidden_lamina_outputs['mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] * (1 - share_hidden_sheath) + curr_hidden_lamina_outputs['max_mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] * (1 - share_hidden_sheath) + curr_hidden_lamina_outputs['Nstruct'] = curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] * (1 - share_hidden_sheath) + curr_hidden_lamina_outputs['sucrose'] = curr_hiddenzone_outputs['sucrose'] * share_leaf * (1 - share_hidden_sheath) + curr_hidden_lamina_outputs['amino_acids'] = curr_hiddenzone_outputs['amino_acids'] * share_leaf * (1 - share_hidden_sheath) + curr_hidden_lamina_outputs['fructan'] = curr_hiddenzone_outputs['fructan'] * share_leaf * (1 - share_hidden_sheath) + curr_hidden_lamina_outputs['proteins'] = curr_hiddenzone_outputs['proteins'] * share_leaf * (1 - share_hidden_sheath) + curr_hidden_lamina_outputs['cytokinins'] = self.model.calculate_init_cytokinins_emerged_tissue(curr_hidden_lamina_outputs['mstruct']) + self.outputs['elements'][hidden_lamina_id] = curr_hidden_lamina_outputs + + # Remove in hiddenzone + curr_hiddenzone_outputs = self.outputs['hiddenzone'][hiddenzone_id] + curr_hiddenzone_outputs['leaf_enclosed_mstruct'] = 0 + curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] = 0 + curr_hiddenzone_outputs['mstruct'] = curr_hiddenzone_outputs['internode_enclosed_mstruct'] + curr_hiddenzone_outputs['Nstruct'] = curr_hiddenzone_outputs['internode_enclosed_Nstruct'] + curr_hiddenzone_outputs['sucrose'] -= curr_hiddenzone_outputs['sucrose'] * share_leaf + curr_hiddenzone_outputs['amino_acids'] -= curr_hiddenzone_outputs['amino_acids'] * share_leaf + curr_hiddenzone_outputs['fructan'] -= curr_hiddenzone_outputs['fructan'] * share_leaf + curr_hiddenzone_outputs['proteins'] -= curr_hiddenzone_outputs['proteins'] * share_leaf + self.outputs['hiddenzone'][hiddenzone_id] = curr_hiddenzone_outputs - else: - # Add to hidden part of the lamina, if any - if share_hidden_sheath < 1: - hidden_lamina_id = hiddenzone_id + tuple(['blade', 'HiddenElement']) - curr_hidden_lamina_outputs = self.outputs['elements'][hidden_lamina_id] - curr_hidden_lamina_outputs['mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] * (1 - share_hidden_sheath) - curr_hidden_lamina_outputs['max_mstruct'] = curr_hiddenzone_outputs['leaf_enclosed_mstruct'] * (1 - share_hidden_sheath) - curr_hidden_lamina_outputs['Nstruct'] = curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] * (1 - share_hidden_sheath) - curr_hidden_lamina_outputs['sucrose'] = curr_hiddenzone_outputs['sucrose'] * share_leaf * (1 - share_hidden_sheath) - curr_hidden_lamina_outputs['amino_acids'] = curr_hiddenzone_outputs['amino_acids'] * share_leaf * (1 - share_hidden_sheath) - curr_hidden_lamina_outputs['fructan'] = curr_hiddenzone_outputs['fructan'] * share_leaf * (1 - share_hidden_sheath) - curr_hidden_lamina_outputs['proteins'] = curr_hiddenzone_outputs['proteins'] * share_leaf * (1 - share_hidden_sheath) - curr_hidden_lamina_outputs['cytokinins'] = self.model.calculate_init_cytokinins_emerged_tissue(curr_hidden_lamina_outputs['mstruct']) - self.outputs['elements'][hidden_lamina_id] = curr_hidden_lamina_outputs - - # Remove in hiddenzone - curr_hiddenzone_outputs = self.outputs['hiddenzone'][hiddenzone_id] - curr_hiddenzone_outputs['leaf_enclosed_mstruct'] = 0 - curr_hiddenzone_outputs['leaf_enclosed_Nstruct'] = 0 - curr_hiddenzone_outputs['mstruct'] = curr_hiddenzone_outputs['internode_enclosed_mstruct'] - curr_hiddenzone_outputs['Nstruct'] = curr_hiddenzone_outputs['internode_enclosed_Nstruct'] - curr_hiddenzone_outputs['sucrose'] -= curr_hiddenzone_outputs['sucrose'] * share_leaf - curr_hiddenzone_outputs['amino_acids'] -= curr_hiddenzone_outputs['amino_acids'] * share_leaf - curr_hiddenzone_outputs['fructan'] -= curr_hiddenzone_outputs['fructan'] * share_leaf - curr_hiddenzone_outputs['proteins'] -= curr_hiddenzone_outputs['proteins'] * share_leaf - self.outputs['hiddenzone'][hiddenzone_id] = curr_hiddenzone_outputs - - # Turn remobilizing flag to False - self.outputs['hiddenzone'][hiddenzone_id]['leaf_is_remobilizing'] = False - - # -- Remobilisation at the end of internode elongation - # Internodes stop to elongate after leaves. We cannot test delta_internode_L > 0 for the cases of short internodes which are mature before GA production. - if hiddenzone_inputs['internode_is_remobilizing']: - - # Add to hidden part of the internode - hidden_internode_id = hiddenzone_id + tuple(['internode', 'HiddenElement']) - if hidden_internode_id not in self.outputs['elements'].keys(): - new_internode_outputs = self.organ_init.__dict__.copy() - self.outputs['elements'][hidden_internode_id] = new_internode_outputs - curr_hidden_internode_outputs = self.outputs['elements'][hidden_internode_id] - curr_hidden_internode_outputs['mstruct'] += curr_hiddenzone_outputs['internode_enclosed_mstruct'] - curr_hidden_internode_outputs['max_mstruct'] = curr_hidden_internode_outputs['mstruct'] - curr_hidden_internode_outputs['Nstruct'] += curr_hiddenzone_outputs['internode_enclosed_Nstruct'] - curr_hidden_internode_outputs['sucrose'] += curr_hiddenzone_outputs['sucrose'] - curr_hidden_internode_outputs['amino_acids'] += curr_hiddenzone_outputs['amino_acids'] - curr_hidden_internode_outputs['fructan'] += curr_hiddenzone_outputs['fructan'] - curr_hidden_internode_outputs['proteins'] += curr_hiddenzone_outputs['proteins'] - curr_hidden_internode_outputs['is_growing'] = False - self.outputs['elements'][hidden_internode_id] = curr_hidden_internode_outputs - - # Turn remobilizing flag to False - self.outputs['hiddenzone'][hiddenzone_id]['internode_is_remobilizing'] = False - - #: Turn the flag to true after remobilisation in order to Delete Hiddenzone in both MTG and shared_outputs - self.outputs['hiddenzone'][hiddenzone_id]['is_over'] = True + # Turn remobilizing flag to False + self.outputs['hiddenzone'][hiddenzone_id]['leaf_is_remobilizing'] = False + + # -- Remobilisation at the end of internode elongation + # Internodes stop to elongate after leaves. We cannot test delta_internode_L > 0 for the cases of short internodes which are mature before GA production. + if hiddenzone_inputs['internode_is_remobilizing']: + + # Add to hidden part of the internode + hidden_internode_id = hiddenzone_id + tuple(['internode', 'HiddenElement']) + if hidden_internode_id not in self.outputs['elements'].keys(): + new_internode_outputs = self.organ_init.__dict__.copy() + self.outputs['elements'][hidden_internode_id] = new_internode_outputs + curr_hidden_internode_outputs = self.outputs['elements'][hidden_internode_id] + curr_hidden_internode_outputs['mstruct'] += curr_hiddenzone_outputs['internode_enclosed_mstruct'] + curr_hidden_internode_outputs['max_mstruct'] = curr_hidden_internode_outputs['mstruct'] + curr_hidden_internode_outputs['Nstruct'] += curr_hiddenzone_outputs['internode_enclosed_Nstruct'] + curr_hidden_internode_outputs['sucrose'] += curr_hiddenzone_outputs['sucrose'] + curr_hidden_internode_outputs['amino_acids'] += curr_hiddenzone_outputs['amino_acids'] + curr_hidden_internode_outputs['fructan'] += curr_hiddenzone_outputs['fructan'] + curr_hidden_internode_outputs['proteins'] += curr_hiddenzone_outputs['proteins'] + curr_hidden_internode_outputs['is_growing'] = False + self.outputs['elements'][hidden_internode_id] = curr_hidden_internode_outputs + + # Turn remobilizing flag to False + self.outputs['hiddenzone'][hiddenzone_id]['internode_is_remobilizing'] = False + + #: Turn the flag to true after remobilisation in order to Delete Hiddenzone in both MTG and shared_outputs + self.outputs['hiddenzone'][hiddenzone_id]['is_over'] = True # -------------------------------- # -------------- Roots ----------- diff --git a/src/openalea/cnwgrass/hydraulics/model.py b/src/openalea/cnwgrass/hydraulics/model.py index 3bf4e0c..a9ee423 100644 --- a/src/openalea/cnwgrass/hydraulics/model.py +++ b/src/openalea/cnwgrass/hydraulics/model.py @@ -390,6 +390,14 @@ def calculate_osmotic_water_potential(fructan, sucrose, amino_acids, volume, tem """ temperature_K = temperature + parameters.CELSIUS_2_KELVIN + #: A pathologically small (just-tracked tiller) element/hiddenzone can have a volume many orders of + #: magnitude below anything MS ever produces, making this division blow up and feed a runaway loop via + #: calculate_delta_turgor_water_potential -> water_potential -> water_influx. Floor at a scale chosen + #: to sit between the smallest observed pathological case (~7.8e-13 m3, a just-tracked tiller element) + #: and the smallest observed legitimate one (~6.3e-10 m3, a genuinely tiny but stable MS sheath + #: element) -- low enough to never engage for real, self-balancing MS/tiller dynamics. + volume = max(volume, 1E-11) + #: Concentration of solutes sucrose = (sucrose * 1E-6) / parameters.NB_C_SUCROSE amino_acids = (amino_acids * 1E-6) / parameters.AMINO_ACIDS_N_RATIO @@ -565,6 +573,9 @@ def calculate_delta_turgor_water_potential(phi, turgor_water_potential, organ_vo elastic_component = (epsilon_x * epsilon_y * epsilon_z) / (epsilon_z * epsilon_x + epsilon_z * epsilon_y + epsilon_x * epsilon_y) #: Elastic reversible growth (MPa) plastic_component = (phi['x'] + phi['y'] + phi['z']) #: Plastic irreversible growth + #: See the identical guard (and its rationale) in calculate_osmotic_water_potential above. + organ_volume = max(organ_volume, 2E-11) + delta_turgor_water_potential = ((1 / ( parameters.RHO_WATER * organ_volume * parameters.VSTORAGE)) * delta_water_content - plastic_component * (max(turgor_water_potential, HiddenZone.PARAMETERS.GAMMA) - HiddenZone.PARAMETERS.GAMMA)) * elastic_component #: (MPa) @@ -859,6 +870,9 @@ def calculate_osmotic_water_potential(sucrose, amino_acids, volume, temperature, """ temperature_K = temperature + parameters.CELSIUS_2_KELVIN + #: See the identical guard (and its rationale) in HiddenZone.calculate_osmotic_water_potential. + volume = max(volume, 1E-11) + #: Concentration of solutes sucrose = (sucrose * 1E-6) / parameters.NB_C_SUCROSE amino_acids = (amino_acids * 1E-6) / parameters.AMINO_ACIDS_N_RATIO @@ -926,6 +940,10 @@ def calculate_delta_turgor_water_potential(volume, delta_water_content): epsilon_z, epsilon_x, epsilon_y = PhotosyntheticOrganElement.PARAMETERS.epsilon['z'], PhotosyntheticOrganElement.PARAMETERS.epsilon['x'], PhotosyntheticOrganElement.PARAMETERS.epsilon['y'] elastic_component = (epsilon_z * epsilon_x * epsilon_y) / (epsilon_z * epsilon_x + epsilon_z * epsilon_y + epsilon_x * epsilon_y) #: Elastic reversible growth (MPa) plastic_component = 0 #: Plastic irreversible growth (MPa) + + #: See the identical guard (and its rationale) in HiddenZone.calculate_osmotic_water_potential. + volume = max(volume, 1E-11) + delta_turgor_water_potential = ((1 / ( parameters.RHO_WATER * volume * parameters.VSTORAGE)) * delta_water_content - plastic_component) * elastic_component #: (MPa) diff --git a/src/openalea/cnwgrass/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index 68039af..748c335 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -249,7 +249,8 @@ class Simulation(object): 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, isolated_roots=False, cnwgrass_roots=True): + def __init__(self, delta_t=1, interpolate_forcing=False, elements_forcing_delta_t=None, hiddenzone_forcing_delta_t=None, isolated_roots=False, cnwgrass_roots=True, + explicit_tillers=False): self.population = model.Population() #: the population to simulate on #: The inputs of the soils. @@ -277,6 +278,7 @@ def __init__(self, delta_t=1, interpolate_forcing=False, elements_forcing_delta_ self.isolated_roots = isolated_roots self.cnwgrass_roots = cnwgrass_roots + self.explicit_tillers = explicit_tillers # set the loggers for compartments and derivatives compartments_logger = logging.getLogger('hydraulics.compartments') @@ -435,7 +437,11 @@ def initialize(self, population, soils): 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 + #: axis.roots/xylem may be shared across several axes of one plant (main stem + explicit + #: tillers, see hydraulics_facade._initialize_model): only the main stem owns a soil + #: entry -- every soil lookup elsewhere in this module is hardcoded to (plant, 'MS'), so a + #: tiller axis is never expected to have its own dedicated soil. + if axis.label == 'MS' and (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) @@ -575,14 +581,23 @@ def run(self): sol = solve_ivp(fun=self._calculate_root_derivatives, t_span=self.time_grid, y0=self.initial_conditions_roots, method='LSODA', t_eval=np.array([self.time_step]), dense_output=False) else: + #: This branch is the only one built to use jac_sparsity: BDF (unlike LSODA) accepts it, and + #: hydraulics_facade._initialize_indices_and_sparsity builds self._jac_sparsity_shoot fresh every + #: call, right before this runs. See that method's docstring for why the true Jacobian is + #: block-diagonal (one block per phytomer) and therefore safe to exploit this way. sol = solve_ivp(fun=self._calculate_shoot_derivatives, t_span=self.time_grid, y0=self.initial_conditions, - method='LSODA', t_eval=np.array([self.time_step]), dense_output=False) + method='BDF', t_eval=np.array([self.time_step]), dense_output=False, + jac_sparsity=self._jac_sparsity_shoot + ) for plant in self.population.plants: - for axis in plant.axes: - axis.xylem.water_potential = (axis.xylem.shoot_root_xylem_conductance * axis.xylem.root_xylem_water_potential - + self.sum_organs_kr_psi) / ( - axis.xylem.shoot_root_xylem_conductance + self.sum_organs_kr) - print("result", axis.xylem.root_xylem_water_potential, axis.xylem.shoot_root_xylem_conductance, axis.xylem.water_potential, self.sum_organs_kr, self.sum_organs_kr_psi / self.sum_organs_kr) + #: TODO: revisit once xylem/phloem pools are split per axis instead of shared plant-wide. + #: axis.xylem is the SAME shared object for every axis of the plant (see + #: hydraulics_facade._build_axis_organs) -- assign once via the main stem instead of + #: redundantly recomputing and rewriting the identical value once per axis. + ms_axis = next(axis for axis in plant.axes if axis.label == 'MS') + ms_axis.xylem.water_potential = (ms_axis.xylem.shoot_root_xylem_conductance * ms_axis.xylem.root_xylem_water_potential + + self.sum_organs_kr_psi) / ( + ms_axis.xylem.shoot_root_xylem_conductance + self.sum_organs_kr) self.nfev_total += sol.nfev @@ -799,11 +814,18 @@ def _calculate_all_derivatives(self, t, y): #: 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) + # Xylem + #: axis.xylem is the SAME shared object for every axis of the plant (see + #: hydraulics_facade._build_axis_organs) -- water_influx/Growth are additive per-axis totals (see + #: Axis.calculate_aggregated_variables), so sum them across every axis before computing the single + #: shared xylem's total water potential once, instead of overwriting it per axis (which silently + #: dropped every axis but whichever was processed last). + plant_water_influx = sum(axis.water_influx for axis in plant.axes) + plant_Growth = sum(axis.Growth for axis in plant.axes) + ms_axis = next(axis for axis in plant.axes if axis.label == 'MS') + ms_axis.xylem.water_potential = ms_axis.xylem.calculate_xylem_water_potential(soil.water_potential, plant_water_influx, plant_Growth, self.delta_t) + for axis in plant.axes: for phytomer in axis.phytomers: # Hidden zone hiddenzone = phytomer.hiddenzone @@ -1407,13 +1429,19 @@ def _calculate_root_derivatives(self, t, y): #: 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) - - # Store water used from soil for eahc axis - soil_water_outputs += (axis.water_influx + axis.Growth) + # Xylem + #: axis.xylem is the SAME shared object for every axis of the plant (see + #: hydraulics_facade._build_axis_organs) -- water_influx/Growth are additive per-axis totals (see + #: Axis.calculate_aggregated_variables), so sum them across every axis before computing the single + #: shared xylem's total water potential once, instead of overwriting it per axis (which silently + #: dropped every axis but whichever was processed last). + plant_water_influx = sum(axis.water_influx for axis in plant.axes) + plant_Growth = sum(axis.Growth for axis in plant.axes) + ms_axis = next(axis for axis in plant.axes if axis.label == 'MS') + ms_axis.xylem.water_potential = ms_axis.xylem.calculate_xylem_water_potential(soil.water_potential, plant_water_influx, plant_Growth, self.delta_t) + + # Store water used from soil for the whole plant + soil_water_outputs += (plant_water_influx + plant_Growth) # compute the derivative of each compartment of soil y_derivatives[self.initial_conditions_mapping_roots[soil]['water_content']] = soil.calculate_water_content_derivative(soil_water_outputs, soil.constant_water_content) diff --git a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py index eaa6d97..a0181b4 100644 --- a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py +++ b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py @@ -80,6 +80,7 @@ def __init__(self, shared_mtg, delta_t, culm_density, update_parameters, shared_elements_inputs_outputs_df, shared_soils_inputs_outputs_df, tillers_replications=None, + explicit_tillers=False, external_soil_model=False, update_shared_df=True, isolated_roots=False, @@ -99,7 +100,9 @@ def __init__(self, shared_mtg, delta_t, culm_density, update_parameters, :param pandas.DataFrame shared_hiddenzones_inputs_outputs_df: the dataframe of inputs and outputs at hiddenzones scale shared between all models. :param pandas.DataFrame shared_elements_inputs_outputs_df: the dataframe of inputs and outputs at elements scale shared between all models. :param pandas.DataFrame shared_soils_inputs_outputs_df: the dataframe of inputs and outputs at soils scale shared between all models. - :param dict [str, float] tillers_replications: a dictionary with tiller id as key, and weight of replication as value. + :param dict [str, float] tillers_replications: a dictionary with tiller id as key, and weight of replication as value. Ignored when `explicit_tillers` is True. + :param bool explicit_tillers: if True, build and integrate a real Axis for every tiller found in the MTG (sharing the main stem's roots/phloem/grains/endosperm) instead of lumping + tillers into the main stem via `tillers_replications` weights. :param bool external_soil_model: whether an external soil model is coupled to cnmetabolism. If True, cnmetabolism will skip calculations made in soil and uptake N by roots :param bool update_shared_df: If `True` update the shared dataframes at init and at each run (unless stated otherwise) @@ -107,6 +110,7 @@ def __init__(self, shared_mtg, delta_t, culm_density, update_parameters, self._shared_mtg = shared_mtg #: the MTG shared between all models self.tillers_replications = tillers_replications + self.explicit_tillers = explicit_tillers self.external_soil_model = external_soil_model self._simulation = cnmetabolism_simulation.Simulation(respiration_model=respiration_model, delta_t=delta_t, culm_density=culm_density, external_soil_model=external_soil_model, isolated_roots=isolated_roots, cnwgrass_roots=cnwgrass_roots) @@ -221,6 +225,187 @@ def graphs(axes_postprocessing_df, hiddenzones_postprocessing_df, organs_postpro meteo_data=meteo_data, graphs_dirpath=graphs_dirpath) + #: axis-scale state variable(s) that are not required to already be present in the MTG when building an + #: Axis: 'mstruct' is entirely self-maintained by CN-Metabolism itself (Simulation.initialize() always + #: recomputes it from the axis's own phytomers via Population.calculate_aggregated_variables(), before + #: anything reads it) rather than being written by another facade -- so it is simply absent for any axis + #: CN-Metabolism has never built before, which is the normal case the first time a tiller axis is seen + #: under explicit_tillers. + AXES_STATE_SELF_MAINTAINED = {'mstruct'} + + def _read_axis_state(self, mtg_axis_vid, mtg_axis_label): + """Create a new CN-Metabolism Axis and read its axis-scale state from the MTG. + + Raises a clear error if a required axis-scale property is missing, instead of a bare KeyError + - relevant in particular for tiller axes under `explicit_tillers`, which are not otherwise touched by this facade. + """ + cnmetabolism_axis = cnmetabolism_model.Axis(mtg_axis_label) + mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) + missing_axis_state = set(cnmetabolism_simulation.Simulation.AXES_STATE) - set(mtg_axis_properties) - self.AXES_STATE_SELF_MAINTAINED + if missing_axis_state: + raise ValueError("Axis '{}' (vertex {}) is missing axis-scale state variable(s) {} in the MTG. " + "explicit_tillers=True requires every tiller axis to carry the same axis-scale state as the main stem " + "(check axes_initial_state.csv / that morphogenesis has been run for this axis).".format( + mtg_axis_label, mtg_axis_vid, sorted(missing_axis_state))) + cnmetabolism_axis_data_dict = {cnmetabolism_axis_data_name: mtg_axis_properties[cnmetabolism_axis_data_name] + for cnmetabolism_axis_data_name in cnmetabolism_simulation.Simulation.AXES_STATE + if cnmetabolism_axis_data_name in mtg_axis_properties} + cnmetabolism_axis.__dict__.update(cnmetabolism_axis_data_dict) + if cnmetabolism_axis.mstruct is None: + #: Placeholder only: overwritten a moment later by Simulation.initialize() -> Population.calculate_aggregated_variables(). + cnmetabolism_axis.mstruct = 0.0 + return cnmetabolism_axis, mtg_axis_properties + + def _build_axis_organs(self, mtg_axis_vid, mtg_axis_properties, cnmetabolism_axis): + """Build the roots/phloem/grains/endosperm organs of `cnmetabolism_axis` from the MTG. Returns whether the axis is valid.""" + is_valid_axis = True + for cnmetabolism_organ_class in (cnmetabolism_model.Roots, cnmetabolism_model.Phloem, cnmetabolism_model.Grains, cnmetabolism_model.Endosperm): + mtg_organ_label = cnmetabolism_converter.CNMETABOLISM_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[cnmetabolism_organ_class] + # create a new organ + cnmetabolism_organ = cnmetabolism_organ_class(mtg_organ_label) + if mtg_organ_label in mtg_axis_properties: + mtg_organ_properties = mtg_axis_properties[mtg_organ_label] + access_mtg_names = cnmetabolism_simulation.Simulation.ORGANS_STATE + if cnmetabolism_organ_class == cnmetabolism_model.Roots and self.isolated_roots: + access_mtg_names += cnmetabolism_simulation.Simulation.ORGANS_FLUXES[:3] + ["Unloading_Sucrose", "Unloading_Amino_Acids"] + cnmetabolism_organ_data_names = set(access_mtg_names).intersection(cnmetabolism_organ.__dict__) + if set(mtg_organ_properties).issuperset(cnmetabolism_organ_data_names): + cnmetabolism_organ_data_dict = {} + for cnmetabolism_organ_data_name in cnmetabolism_organ_data_names: + cnmetabolism_organ_data_dict[cnmetabolism_organ_data_name] = mtg_organ_properties[cnmetabolism_organ_data_name] + + # Debug: Tell if missing input variable + if math.isnan(mtg_organ_properties[cnmetabolism_organ_data_name]) or mtg_organ_properties[cnmetabolism_organ_data_name] is None: + print('Missing variable', cnmetabolism_organ_data_name, 'for vertex id', mtg_axis_vid, 'which is', mtg_organ_label) + + cnmetabolism_organ.__dict__.update(cnmetabolism_organ_data_dict) + if mtg_organ_label == 'roots' and self.external_soil_model: + cnmetabolism_organ.Uptake_Nitrates = mtg_organ_properties['Uptake_Nitrates'] + cnmetabolism_organ.HATS_LATS = mtg_organ_properties['HATS_LATS'] + + # Update parameters if specified + if mtg_organ_label in self._update_parameters: + cnmetabolism_organ.PARAMETERS.__dict__.update(self._update_parameters[mtg_organ_label]) + + cnmetabolism_organ.initialize() + # add the new organ to current axis + setattr(cnmetabolism_axis, mtg_organ_label, cnmetabolism_organ) + + elif cnmetabolism_organ_class is not cnmetabolism_model.Grains: + is_valid_axis = False + break + + # For the 1st instantiation of the Grains class during a simulation covering vegetative and reproductive stages + elif cnmetabolism_organ_class is cnmetabolism_model.Grains: + if mtg_axis_properties['status'] != 'reproductive': + continue + # grains = cnmetabolism_model.Grains(cnmetabolism_converter.CNMETABOLISM_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[cnmetabolism_model.Grains]) + # grains.initialize() + # setattr(cnmetabolism_axis, cnmetabolism_converter.CNMETABOLISM_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[cnmetabolism_model.Grains], grains) + + elif cnmetabolism_organ_class is cnmetabolism_model.Endosperm: + continue + + else: + is_valid_axis = False + print('Invalid axis because of {}'.format(cnmetabolism_organ_class)) + break + return is_valid_axis + + def _build_axis_phytomers(self, mtg_axis_vid, cnmetabolism_axis, Tair, cohorts, cohorts_replications): + """Build the phytomers (hiddenzones/organs/elements) of `cnmetabolism_axis` from the MTG. Returns whether the axis has at least one valid phytomer.""" + has_valid_phytomer = False + for mtg_metamer_vid in self._shared_mtg.components_iter(mtg_axis_vid): + mtg_metamer_index = int(self._shared_mtg.index(mtg_metamer_vid)) + + # create a new phytomer + cnmetabolism_phytomer = cnmetabolism_model.Phytomer(mtg_metamer_index, cohorts=cohorts, cohorts_replications=cohorts_replications) #: Hack to treat tillering cases :TEMPORARY + + mtg_hiddenzone_label = cnmetabolism_converter.CNMETABOLISM_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[cnmetabolism_model.HiddenZone] + mtg_metamer_properties = self._shared_mtg.get_vertex_property(mtg_metamer_vid) + + if mtg_hiddenzone_label in mtg_metamer_properties: + mtg_hiddenzone_properties = mtg_metamer_properties[mtg_hiddenzone_label] + + if set(mtg_hiddenzone_properties).issuperset(cnmetabolism_simulation.Simulation.HIDDENZONE_STATE) and not mtg_hiddenzone_properties['is_over']: + has_valid_hiddenzone = True + cnmetabolism_hiddenzone_data_dict = {} + for cnmetabolism_hiddenzone_data_name in cnmetabolism_simulation.Simulation.HIDDENZONE_STATE: + cnmetabolism_hiddenzone_data_dict[cnmetabolism_hiddenzone_data_name] = mtg_hiddenzone_properties[cnmetabolism_hiddenzone_data_name] + + # create a new hiddenzone + cnmetabolism_hiddenzone = cnmetabolism_model.HiddenZone(mtg_hiddenzone_label, cohorts=cohorts, cohorts_replications=cohorts_replications, index=cnmetabolism_phytomer.index, + **cnmetabolism_hiddenzone_data_dict) + + # Update parameters if specified + if mtg_hiddenzone_label in self._update_parameters: + cnmetabolism_hiddenzone.PARAMETERS.__dict__.update(self._update_parameters[mtg_hiddenzone_label]) + + cnmetabolism_hiddenzone.initialize() + # add the new hiddenzone to current phytomer + setattr(cnmetabolism_phytomer, mtg_hiddenzone_label, cnmetabolism_hiddenzone) + else: + has_valid_hiddenzone = False + else: + has_valid_hiddenzone = False + + has_valid_organ = False + for mtg_organ_vid in self._shared_mtg.components_iter(mtg_metamer_vid): + mtg_organ_label = self._shared_mtg.label(mtg_organ_vid) + if mtg_organ_label not in MTG_TO_CNMETABOLISM_PHYTOMERS_ORGANS_MAPPING or self._shared_mtg.get_vertex_property(mtg_organ_vid)['length'] == 0: + continue + + # create a new organ + cnmetabolism_organ_class = MTG_TO_CNMETABOLISM_PHYTOMERS_ORGANS_MAPPING[mtg_organ_label] + cnmetabolism_organ = cnmetabolism_organ_class(mtg_organ_label) + + # Update parameters if specified + if 'PhotosyntheticOrgan' in self._update_parameters: + cnmetabolism_organ.PARAMETERS.__dict__.update(self._update_parameters['PhotosyntheticOrgan']) + + cnmetabolism_organ.initialize() + has_valid_element = False + + # Create a new element + for mtg_element_vid in self._shared_mtg.components_iter(mtg_organ_vid): + mtg_element_properties = self._shared_mtg.get_vertex_property(mtg_element_vid) + mtg_element_label = self._shared_mtg.label(mtg_element_vid) + if mtg_element_label not in cnmetabolism_converter.DATAFRAME_TO_CNMETABOLISM_ELEMENTS_NAMES_MAPPING \ + or (self._shared_mtg.get_vertex_property(mtg_element_vid)['length'] == 0) \ + or (self._shared_mtg.get_vertex_property(mtg_element_vid).get('mstruct', 0) == 0) \ + or ((mtg_element_label == 'HiddenElement') and (self._shared_mtg.get_vertex_property(mtg_element_vid).get('is_growing', True))) \ + or (self._shared_mtg.get_vertex_property(mtg_element_vid).get('green_area', 0) <= 0.25E-6): + continue # TODO: Check that we are not taking out some relevant cases with the condition on mstruct == 0 + + has_valid_element = True + cnmetabolism_element_data_dict = {} + for cnmetabolism_element_data_name in cnmetabolism_simulation.Simulation.ELEMENTS_STATE: + mtg_element_data_value = mtg_element_properties.get(cnmetabolism_element_data_name) + # In case the value is None, or the property is not even defined, we take default value from InitCompartment + if mtg_element_data_value is None or np.isnan(mtg_element_data_value): + if cnmetabolism_element_data_name == 'Ts': + mtg_element_data_value = Tair + else: + mtg_element_data_value = cnmetabolism_parameters.PhotosyntheticOrganElementInitCompartments().__dict__[cnmetabolism_element_data_name] + cnmetabolism_element_data_dict[cnmetabolism_element_data_name] = mtg_element_data_value + cnmetabolism_element = CNMETABOLISM_ORGANS_TO_ELEMENTS_MAPPING[cnmetabolism_organ_class](mtg_element_label, cohorts=cohorts, cohorts_replications=cohorts_replications, + index=cnmetabolism_phytomer.index, **cnmetabolism_element_data_dict) + # Add parameters from organ scale + cnmetabolism_element.PARAMETERS.__dict__.update(cnmetabolism_organ.PARAMETERS.__dict__) + + # add the new element to current organ + setattr(cnmetabolism_organ, cnmetabolism_converter.DATAFRAME_TO_CNMETABOLISM_ELEMENTS_NAMES_MAPPING[mtg_element_label], cnmetabolism_element) + + if has_valid_element: + has_valid_organ = True + setattr(cnmetabolism_phytomer, CNMETABOLISM_ATTRIBUTES_MAPPING[cnmetabolism_organ_class], cnmetabolism_organ) + + if has_valid_organ or has_valid_hiddenzone: + cnmetabolism_axis.phytomers.append(cnmetabolism_phytomer) + has_valid_phytomer = True + + return has_valid_phytomer + def _initialize_model(self, Tair, Tsoil): """ Initialize the inputs of the model from the MTG shared between all models and the soils. @@ -229,9 +414,10 @@ def _initialize_model(self, Tair, Tsoil): :param float Tsoil: soil temperature (�C) """ - # Convert number of replications per tiller into number of replications per cohort + # Convert number of replications per tiller into number of replications per cohort. + # This is the MS + cohort-weighting shortcut ; ignored (kept empty) when self.explicit_tillers is True. cohorts_replications = {} - if self.tillers_replications is not None: + if not self.explicit_tillers and self.tillers_replications is not None: for tiller_id, replication_weight in self.tillers_replications.items(): try: tiller_rank = int(tiller_id[1:]) @@ -247,180 +433,78 @@ def _initialize_model(self, Tair, Tsoil): # create a new plant cnmetabolism_plant = cnmetabolism_model.Plant(mtg_plant_index) is_valid_plant = False + ms_axis = None + tiller_axis_vids = [] #: (mtg_axis_vid, mtg_axis_label) pairs, only used when self.explicit_tillers is True for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): mtg_axis_label = self._shared_mtg.label(mtg_axis_vid) + if isinstance(mtg_axis_label, bytes): + #: Some MTGs carry byte-string axis labels (see morphogenesis_facade for the same guard); + #: normalize to str so 'MS'/tiller-rank comparisons below (and axis.label == 'MS' checks + #: throughout cnmetabolism/simulation.py) behave consistently regardless of the MTG source. + mtg_axis_label = mtg_axis_label.decode('UTF-8') - #: Hack to deal with tillering cases : TEMPORARY if mtg_axis_label != 'MS': - try: - tiller_rank = int(mtg_axis_label[1:]) - cnmetabolism_plant.cohorts.append(tiller_rank + 3) - continue - except ValueError: - continue + if self.explicit_tillers: + #: Real per-tiller simulation: this axis is built in the second pass below, once MS's shared + #: roots/phloem/grains/endosperm are available to attach it to (the MTG does not carry + #: independent roots/phloem/grains/endosperm per tiller: there is a single below-ground + #: root system and a single phloem pool per plant). + tiller_axis_vids.append((mtg_axis_vid, mtg_axis_label)) + else: + #: Hack to deal with tillering cases : TEMPORARY + try: + tiller_rank = int(mtg_axis_label[1:]) + cnmetabolism_plant.cohorts.append(tiller_rank + 3) + except ValueError: + pass + continue #: Main Stem - # create a new axis - cnmetabolism_axis = cnmetabolism_model.Axis(mtg_axis_label) - mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) - cnmetabolism_axis_data_dict = {} - for cnmetabolism_axis_data_name in cnmetabolism_simulation.Simulation.AXES_STATE: - cnmetabolism_axis_data_dict[cnmetabolism_axis_data_name] = mtg_axis_properties[cnmetabolism_axis_data_name] - cnmetabolism_axis.__dict__.update(cnmetabolism_axis_data_dict) - is_valid_axis = True - for cnmetabolism_organ_class in (cnmetabolism_model.Roots, cnmetabolism_model.Phloem, cnmetabolism_model.Grains, cnmetabolism_model.Endosperm): - mtg_organ_label = cnmetabolism_converter.CNMETABOLISM_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[cnmetabolism_organ_class] - # create a new organ - cnmetabolism_organ = cnmetabolism_organ_class(mtg_organ_label) - if mtg_organ_label in mtg_axis_properties: - mtg_organ_properties = mtg_axis_properties[mtg_organ_label] - access_mtg_names = cnmetabolism_simulation.Simulation.ORGANS_STATE - if cnmetabolism_organ_class == cnmetabolism_model.Roots and self.isolated_roots: - access_mtg_names += cnmetabolism_simulation.Simulation.ORGANS_FLUXES[:3] + ["Unloading_Sucrose", "Unloading_Amino_Acids"] - cnmetabolism_organ_data_names = set(access_mtg_names).intersection(cnmetabolism_organ.__dict__) - if set(mtg_organ_properties).issuperset(cnmetabolism_organ_data_names): - cnmetabolism_organ_data_dict = {} - for cnmetabolism_organ_data_name in cnmetabolism_organ_data_names: - cnmetabolism_organ_data_dict[cnmetabolism_organ_data_name] = mtg_organ_properties[cnmetabolism_organ_data_name] - - # Debug: Tell if missing input variable - if math.isnan(mtg_organ_properties[cnmetabolism_organ_data_name]) or mtg_organ_properties[cnmetabolism_organ_data_name] is None: - print('Missing variable', cnmetabolism_organ_data_name, 'for vertex id', mtg_axis_vid, 'which is', mtg_organ_label) - - cnmetabolism_organ.__dict__.update(cnmetabolism_organ_data_dict) - if mtg_organ_label == 'roots' and self.external_soil_model: - cnmetabolism_organ.Uptake_Nitrates = mtg_organ_properties['Uptake_Nitrates'] - cnmetabolism_organ.HATS_LATS = mtg_organ_properties['HATS_LATS'] - - # Update parameters if specified - if mtg_organ_label in self._update_parameters: - cnmetabolism_organ.PARAMETERS.__dict__.update(self._update_parameters[mtg_organ_label]) - - cnmetabolism_organ.initialize() - # add the new organ to current axis - setattr(cnmetabolism_axis, mtg_organ_label, cnmetabolism_organ) - - elif cnmetabolism_organ_class is not cnmetabolism_model.Grains: - is_valid_axis = False - break - - # For the 1st instantiation of the Grains class during a simulation covering vegetative and reproductive stages - elif cnmetabolism_organ_class is cnmetabolism_model.Grains: - if mtg_axis_properties['status'] != 'reproductive': - continue - # grains = cnmetabolism_model.Grains(cnmetabolism_converter.CNMETABOLISM_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[cnmetabolism_model.Grains]) - # grains.initialize() - # setattr(cnmetabolism_axis, cnmetabolism_converter.CNMETABOLISM_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[cnmetabolism_model.Grains], grains) - - elif cnmetabolism_organ_class is cnmetabolism_model.Endosperm: - continue - - else: - is_valid_axis = False - print('Invalid axis because of {}'.format(cnmetabolism_organ_class)) - break + cnmetabolism_axis, mtg_axis_properties = self._read_axis_state(mtg_axis_vid, mtg_axis_label) + is_valid_axis = self._build_axis_organs(mtg_axis_vid, mtg_axis_properties, cnmetabolism_axis) if not is_valid_axis: continue - has_valid_phytomer = False - for mtg_metamer_vid in self._shared_mtg.components_iter(mtg_axis_vid): - mtg_metamer_index = int(self._shared_mtg.index(mtg_metamer_vid)) - - # create a new phytomer - cnmetabolism_phytomer = cnmetabolism_model.Phytomer(mtg_metamer_index, cohorts=cnmetabolism_plant.cohorts, cohorts_replications=cohorts_replications) #: Hack to treat tillering cases :TEMPORARY - - mtg_hiddenzone_label = cnmetabolism_converter.CNMETABOLISM_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[cnmetabolism_model.HiddenZone] - mtg_metamer_properties = self._shared_mtg.get_vertex_property(mtg_metamer_vid) - - if mtg_hiddenzone_label in mtg_metamer_properties: - mtg_hiddenzone_properties = mtg_metamer_properties[mtg_hiddenzone_label] - - if set(mtg_hiddenzone_properties).issuperset(cnmetabolism_simulation.Simulation.HIDDENZONE_STATE) and not mtg_hiddenzone_properties['is_over']: - has_valid_hiddenzone = True - cnmetabolism_hiddenzone_data_dict = {} - for cnmetabolism_hiddenzone_data_name in cnmetabolism_simulation.Simulation.HIDDENZONE_STATE: - cnmetabolism_hiddenzone_data_dict[cnmetabolism_hiddenzone_data_name] = mtg_hiddenzone_properties[cnmetabolism_hiddenzone_data_name] - - # create a new hiddenzone - cnmetabolism_hiddenzone = cnmetabolism_model.HiddenZone(mtg_hiddenzone_label, cohorts=cnmetabolism_plant.cohorts, cohorts_replications=cohorts_replications, index=cnmetabolism_phytomer.index, - **cnmetabolism_hiddenzone_data_dict) - - # Update parameters if specified - if mtg_hiddenzone_label in self._update_parameters: - cnmetabolism_hiddenzone.PARAMETERS.__dict__.update(self._update_parameters[mtg_hiddenzone_label]) - - cnmetabolism_hiddenzone.initialize() - # add the new hiddenzone to current phytomer - setattr(cnmetabolism_phytomer, mtg_hiddenzone_label, cnmetabolism_hiddenzone) - else: - has_valid_hiddenzone = False - else: - has_valid_hiddenzone = False - - has_valid_organ = False - for mtg_organ_vid in self._shared_mtg.components_iter(mtg_metamer_vid): - mtg_organ_label = self._shared_mtg.label(mtg_organ_vid) - if mtg_organ_label not in MTG_TO_CNMETABOLISM_PHYTOMERS_ORGANS_MAPPING or self._shared_mtg.get_vertex_property(mtg_organ_vid)['length'] == 0: - continue - - # create a new organ - cnmetabolism_organ_class = MTG_TO_CNMETABOLISM_PHYTOMERS_ORGANS_MAPPING[mtg_organ_label] - cnmetabolism_organ = cnmetabolism_organ_class(mtg_organ_label) - - # Update parameters if specified - if 'PhotosyntheticOrgan' in self._update_parameters: - cnmetabolism_organ.PARAMETERS.__dict__.update(self._update_parameters['PhotosyntheticOrgan']) - - cnmetabolism_organ.initialize() - has_valid_element = False - - # Create a new element - for mtg_element_vid in self._shared_mtg.components_iter(mtg_organ_vid): - mtg_element_properties = self._shared_mtg.get_vertex_property(mtg_element_vid) - mtg_element_label = self._shared_mtg.label(mtg_element_vid) - if mtg_element_label not in cnmetabolism_converter.DATAFRAME_TO_CNMETABOLISM_ELEMENTS_NAMES_MAPPING \ - or (self._shared_mtg.get_vertex_property(mtg_element_vid)['length'] == 0) \ - or (self._shared_mtg.get_vertex_property(mtg_element_vid).get('mstruct', 0) == 0) \ - or ((mtg_element_label == 'HiddenElement') and (self._shared_mtg.get_vertex_property(mtg_element_vid).get('is_growing', True))) \ - or (self._shared_mtg.get_vertex_property(mtg_element_vid).get('green_area', 0) <= 0.25E-6): - continue # TODO: Check that we are not taking out some relevant cases with the condition on mstruct == 0 - - has_valid_element = True - cnmetabolism_element_data_dict = {} - for cnmetabolism_element_data_name in cnmetabolism_simulation.Simulation.ELEMENTS_STATE: - mtg_element_data_value = mtg_element_properties.get(cnmetabolism_element_data_name) - # In case the value is None, or the property is not even defined, we take default value from InitCompartment - if mtg_element_data_value is None or np.isnan(mtg_element_data_value): - if cnmetabolism_element_data_name == 'Ts': - mtg_element_data_value = Tair - else: - mtg_element_data_value = cnmetabolism_parameters.PhotosyntheticOrganElementInitCompartments().__dict__[cnmetabolism_element_data_name] - cnmetabolism_element_data_dict[cnmetabolism_element_data_name] = mtg_element_data_value - cnmetabolism_element = CNMETABOLISM_ORGANS_TO_ELEMENTS_MAPPING[cnmetabolism_organ_class](mtg_element_label, cohorts=cnmetabolism_plant.cohorts, cohorts_replications=cohorts_replications, - index=cnmetabolism_phytomer.index, **cnmetabolism_element_data_dict) - # Add parameters from organ scale - cnmetabolism_element.PARAMETERS.__dict__.update(cnmetabolism_organ.PARAMETERS.__dict__) - - # add the new element to current organ - setattr(cnmetabolism_organ, cnmetabolism_converter.DATAFRAME_TO_CNMETABOLISM_ELEMENTS_NAMES_MAPPING[mtg_element_label], cnmetabolism_element) - - if has_valid_element: - has_valid_organ = True - setattr(cnmetabolism_phytomer, CNMETABOLISM_ATTRIBUTES_MAPPING[cnmetabolism_organ_class], cnmetabolism_organ) - - if has_valid_organ or has_valid_hiddenzone: - cnmetabolism_axis.phytomers.append(cnmetabolism_phytomer) - has_valid_phytomer = True + has_valid_phytomer = self._build_axis_phytomers(mtg_axis_vid, cnmetabolism_axis, Tair, cnmetabolism_plant.cohorts, cohorts_replications) if not has_valid_phytomer: is_valid_axis = False if is_valid_axis: cnmetabolism_plant.axes.append(cnmetabolism_axis) + ms_axis = cnmetabolism_axis is_valid_plant = True + #: Second pass: real tiller axes, sharing the main stem's roots/phloem/grains/endosperm. + if self.explicit_tillers and tiller_axis_vids: + if ms_axis is None: + raise ValueError("explicit_tillers=True but no valid main stem (MS) axis was built for plant {}; " + "tillers cannot be attached to shared roots/phloem/grains/endosperm.".format(mtg_plant_index)) + for mtg_axis_vid, mtg_axis_label in tiller_axis_vids: + try: + cnmetabolism_tiller_axis, _ = self._read_axis_state(mtg_axis_vid, mtg_axis_label) + except ValueError: + #: A tiller freshly created by morphogenesis this same time step may not yet carry its + #: axis-scale state (e.g. SAM_temperature/nb_leaves/status not seeded until the next + #: morphogenesis pass). Skip it for this time step rather than aborting the whole run -- + #: exactly like an incomplete phytomer/hiddenzone/organ is skipped elsewhere below -- + #: it will be picked up once morphogenesis has fully initialized it. + continue + cnmetabolism_tiller_axis.roots = ms_axis.roots + cnmetabolism_tiller_axis.phloem = ms_axis.phloem + cnmetabolism_tiller_axis.grains = ms_axis.grains + cnmetabolism_tiller_axis.endosperm = ms_axis.endosperm + + #: cohorts=[] / cohorts_replications=None here so that phytomer.nb_replications == 1 : each real + #: tiller phytomer is now its own axis-scale object, it must not also be scaled by a cohort weight. + has_valid_phytomer = self._build_axis_phytomers(mtg_axis_vid, cnmetabolism_tiller_axis, Tair, [], None) + + if has_valid_phytomer: + cnmetabolism_plant.axes.append(cnmetabolism_tiller_axis) + is_valid_plant = True + if is_valid_plant: self.population.plants.append(cnmetabolism_plant) @@ -438,7 +522,11 @@ def _initialize_indices_and_sparcity(self): icm = self._simulation.initial_conditions_mapping for plant in self._simulation.population.plants: for axis in plant.axes: - axis._phloem_contributors = [axis.roots] + #: axis.phloem (and axis.roots) may be the SAME shared object across several axes of one plant + #: (main stem + explicit tillers): build the contributor list once per plant, on the shared + #: phloem object itself, so every axis referencing it sees (and extends) the same merged list. + if not hasattr(axis.phloem, '_contributors'): + axis.phloem._contributors = [axis.roots] for variable in ('sucrose', 'amino_acids'): setattr(axis.phloem, f"_i_{variable}", icm[axis.phloem][variable]) @@ -457,8 +545,17 @@ def _initialize_indices_and_sparcity(self): if axis.endosperm is not None: for variable in ('moistening', 'starch', 'proteins'): setattr(axis.endosperm, f"_i_{variable}", icm[axis.endosperm][variable]) - - # TODO include endosperm in jacobian sparcity matrix + #: Phloem.calculate_sucrose_derivative/calculate_amino_acids_derivative both special-case an + #: Endosperm contributor (sucrose += D_starch, amino_acids += D_proteins, see + #: cnmetabolism/model.py's Phloem), but it was never actually added to axis.phloem._contributors + #: -- meaning that coupling was silently never applied, and the phloem rows never declared a + #: dependency on endosperm's starch/proteins columns. axis.endosperm may be the SAME shared + #: object across several axes of one plant (main stem + explicit tillers): only wire it up once. + if not hasattr(axis.endosperm, '_sparsity_built'): + axis.endosperm._sparsity_built = True + axis.phloem._contributors.append(axis.endosperm) + S[i_ph_suc, icm[axis.endosperm]['starch']] = True + S[i_ph_aa, icm[axis.endosperm]['proteins']] = True for phytomer in axis.phytomers: hiddenzone = phytomer.hiddenzone @@ -475,7 +572,7 @@ def _initialize_indices_and_sparcity(self): HZ_suc_cols.append(icm[hiddenzone]['sucrose']) HZ_aa_cols.append(icm[hiddenzone]['amino_acids']) - axis._phloem_contributors.append(hiddenzone) + axis.phloem._contributors.append(hiddenzone) for organ in (phytomer.chaff, phytomer.peduncle, phytomer.lamina, phytomer.internode, phytomer.sheath): if organ is not None: @@ -505,7 +602,7 @@ def _initialize_indices_and_sparcity(self): S[i_e_aa, icm[hiddenzone]['amino_acids']] = True if not element.is_growing: - axis._phloem_contributors.append(element) + axis.phloem._contributors.append(element) # Couplings that are easy to forget (make these dense): # a) HZ rows depend on element loadings → HZ rows depend on element sucrose/AA @@ -519,7 +616,10 @@ def _initialize_indices_and_sparcity(self): S.rows[i_ph_aa].extend(E_aa_cols + HZ_aa_cols); S.data[i_ph_aa].extend([True]*(len(E_aa_cols) + len(HZ_aa_cols))) # NOTE: likely not to happen in Wheat-BRIDGES simulations for now - if axis.grains is not None: + #: axis.grains may be the SAME shared object across several axes of one plant (main stem + + #: explicit tillers): only build its sparsity entries once, to avoid duplicating row entries. + if axis.grains is not None and not hasattr(axis.grains, '_sparsity_built'): + axis.grains._sparsity_built = True grain_idxs = [] for variable in ('structure','starch','proteins','age_from_flowering'): setattr(axis.grains, f"_i_{variable}", icm[axis.grains][variable]) @@ -527,8 +627,15 @@ def _initialize_indices_and_sparcity(self): for i in grain_idxs: S.rows[i].extend(grain_idxs); S.data[i].extend([True]*len(grain_idxs)) S[i, i_ph_suc] = True; S[i, i_ph_aa] = True - # grains contribute to phloem via AA (if any) - S[i_ph_aa, icm[axis.grains]['proteins']] = True # harmless over-approx + #: Phloem.calculate_sucrose_derivative/calculate_amino_acids_derivative both special-case a + #: Grains contributor (sucrose -= S_grain_structure + S_grain_starch*structural_dry_mass, + #: amino_acids -= S_Proteins, see cnmetabolism/model.py's Phloem), but grains was never added + #: to axis.phloem._contributors -- that coupling was silently never applied. Also declare the + #: reverse (phloem row depends on grains columns) sparsity links -- only the AA one existed. + axis.phloem._contributors.append(axis.grains) + S[i_ph_suc, icm[axis.grains]['structure']] = True + S[i_ph_suc, icm[axis.grains]['starch']] = True + S[i_ph_aa, icm[axis.grains]['proteins']] = True self._simulation._jac_sparsity_shoot = S.tocsr() @@ -559,7 +666,10 @@ def _update_shared_MTG(self): cnmetabolism_axis_label = cnmetabolism_axis.label while True: mtg_axis_vid = next(mtg_axes_iterator) - if self._shared_mtg.label(mtg_axis_vid) == cnmetabolism_axis_label: + found_axis_label = self._shared_mtg.label(mtg_axis_vid) + if isinstance(found_axis_label, bytes): + found_axis_label = found_axis_label.decode('UTF-8') + if found_axis_label == cnmetabolism_axis_label: break cnmetabolism_axis_property_names = [property_name for property_name in cnmetabolism_simulation.Simulation.AXES_RUN_VARIABLES if hasattr(cnmetabolism_axis, property_name)] @@ -567,18 +677,22 @@ def _update_shared_MTG(self): cnmetabolism_axis_property_value = getattr(cnmetabolism_axis, cnmetabolism_axis_property_name) self._shared_mtg.property(cnmetabolism_axis_property_name)[mtg_axis_vid] = cnmetabolism_axis_property_value - for mtg_organ_label in MTG_TO_CNMETABOLISM_AXES_ORGANS_MAPPING.keys(): - cnmetabolism_organ = getattr(cnmetabolism_axis, mtg_organ_label) - if cnmetabolism_organ is None: - continue - elif mtg_organ_label not in self._shared_mtg.get_vertex_property(mtg_axis_vid) and cnmetabolism_organ is not None: - # Add a property describing the organ to the current axis of the MTG - self._shared_mtg.property(mtg_organ_label)[mtg_axis_vid] = {} - # Update the property describing the organ of the current axis in the MTG - mtg_organ_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid)[mtg_organ_label] - for cnmetabolism_property_name in cnmetabolism_simulation.Simulation.ORGANS_RUN_VARIABLES: - if hasattr(cnmetabolism_organ, cnmetabolism_property_name): - mtg_organ_properties[cnmetabolism_property_name] = getattr(cnmetabolism_organ, cnmetabolism_property_name) + #: roots/phloem/grains/endosperm are shared objects across every axis of the plant (see + #: _build_axis_organs) -- write them back to the MTG only once, under MS, so they are not + #: duplicated (with byte-identical values) under every tiller axis in the outputs. + if cnmetabolism_axis_label == 'MS': + for mtg_organ_label in MTG_TO_CNMETABOLISM_AXES_ORGANS_MAPPING.keys(): + cnmetabolism_organ = getattr(cnmetabolism_axis, mtg_organ_label) + if cnmetabolism_organ is None: + continue + elif mtg_organ_label not in self._shared_mtg.get_vertex_property(mtg_axis_vid) and cnmetabolism_organ is not None: + # Add a property describing the organ to the current axis of the MTG + self._shared_mtg.property(mtg_organ_label)[mtg_axis_vid] = {} + # Update the property describing the organ of the current axis in the MTG + mtg_organ_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid)[mtg_organ_label] + for cnmetabolism_property_name in cnmetabolism_simulation.Simulation.ORGANS_RUN_VARIABLES: + if hasattr(cnmetabolism_organ, cnmetabolism_property_name): + mtg_organ_properties[cnmetabolism_property_name] = getattr(cnmetabolism_organ, cnmetabolism_property_name) mtg_metamers_iterator = self._shared_mtg.components_iter(mtg_axis_vid) for cnmetabolism_phytomer in cnmetabolism_axis.phytomers: cnmetabolism_phytomer_index = cnmetabolism_phytomer.index diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index dd2b05d..5ce07fb 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -110,7 +110,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, # Canopy parameters single_plant = False, plant_density = {1: 250}, inter_row = 0.15, sowing_depth = 0.025, N_fertilizations = None, # Options - tillers_replications=None, stored_times = None, computing_light_interception=True, external_soil_model=False, heterogeneous_canopy=True, show_3Dplant = False, + tillers_replications=None, explicit_tillers=False, stored_times = None, computing_light_interception=True, external_soil_model=False, heterogeneous_canopy=True, show_3Dplant = False, hydraulics = False, stomatal_model_name='BWB', drought_trigger=None, rehydration_scenario=None, optimal_growth_option=False, option_static = False, isolated_roots = False, cnwgrass_roots = True, UPDATE_SHARED_DF=False, START_TIME = 0, CARIBU_TIMESTEP = 4, MORPHOGENESIS_TIMESTEP = 1, GROWTH_TIMESTEP = 1, CNMETABOLISM_TIMESTEP = 1, SENESCENCE_TIMESTEP = 1, HYDRAULICS_TIMESTEP = 1): @@ -143,6 +143,11 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, :param dict or None N_fertilizations: nitrogen fertilization events applied to the soil nitrate pool. Either {time_step_in_hours: amount of nitrates added (umol) at that hour, ...}, or {'constant_Conc_Nitrates': value} to force a constant soil nitrate concentration instead of punctual additions. Only used if `cnwgrass_roots` is True. :param dict [str, float] or None tillers_replications: a dictionary with tiller id as key, and weight of replication as value, used to scale up CN-Metabolism fluxes for unreplicated tillers. + Ignored by CN-Metabolism when `explicit_tillers` is True. + :param bool explicit_tillers: if True, CN-Metabolism builds and integrates a real :class:`Axis` for every tiller present in the MTG (sharing the roots/phloem/grains/endosperm of the main + stem, since those organs are not duplicated per tiller in the MTG), instead of lumping tillers into the main stem via `tillers_replications` weights. + Also makes Gas-Exchange compute real photosynthesis/transpiration, Growth compute real dimensional/mass growth, and Senescence compute real senescence, + for every tiller instead of only the main stem's. :param str or list or None stored_times: time steps at which the model outputs are stored. Either 'all', a list of time steps (hours), or an empty list. :param bool computing_light_interception: whether to run the Caribu facade to compute light interception; if False, organs keep whatever `PARa`/`Erel` they were initialised with. :param bool external_soil_model: whether an external soil model is coupled to cnmetabolism. If True, cnmetabolism will skip its own soil calculations and expect root N uptake to be forced @@ -192,7 +197,9 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.N_fertilizations = N_fertilizations # plant parameters + tillers_replications = tillers_replications if not explicit_tillers else None self.tillers_replications = tillers_replications + self.explicit_tillers = explicit_tillers # logging and data structures self.stored_times = stored_times @@ -273,7 +280,8 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, optimal_growth_option=optimal_growth_option, option_static=option_static, update_parameters=update_parameters_morphogenesis, - update_shared_df=UPDATE_SHARED_DF) + update_shared_df=UPDATE_SHARED_DF, + explicit_tillers=explicit_tillers) # -- CARIBU -- if self.computing_light_interception: @@ -311,6 +319,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.shared_organs_inputs_outputs_df, self.shared_axes_inputs_outputs_df, self.shared_elements_inputs_outputs_df, + explicit_tillers=explicit_tillers, update_parameters=update_parameters_senescence, update_shared_df=UPDATE_SHARED_DF, cnwgrass_roots=cnwgrass_roots) @@ -333,6 +342,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.shared_elements_inputs_outputs_df, stomatal_model_name=stomatal_model_name, hydraulics=hydraulics, + explicit_tillers=explicit_tillers, update_parameters=update_parameters_gasexchange, update_shared_df=UPDATE_SHARED_DF) @@ -361,6 +371,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.shared_elements_inputs_outputs_df, self.shared_axes_inputs_outputs_df, hydraulics=hydraulics, + explicit_tillers=explicit_tillers, update_parameters=update_parameters_growth, update_shared_df=UPDATE_SHARED_DF, cnwgrass_roots=cnwgrass_roots) @@ -414,6 +425,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.shared_elements_inputs_outputs_df, self.shared_soils_inputs_outputs_df, tillers_replications=tillers_replications, + explicit_tillers=explicit_tillers, external_soil_model=external_soil_model, update_shared_df=UPDATE_SHARED_DF, isolated_roots=isolated_roots, @@ -484,7 +496,8 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.shared_soils_inputs_outputs_df, update_shared_df=UPDATE_SHARED_DF, isolated_roots=isolated_roots, - cnwgrass_roots=cnwgrass_roots) + cnwgrass_roots=cnwgrass_roots, + explicit_tillers=explicit_tillers) # -- MODEL INTEGRATION -- # Facade initialisation @@ -566,7 +579,12 @@ def sync_shoot_outputs_with_root_mtg(self): # print("sent potential", self.props[name][1]) elif name == "mstruct_axis": - self.props[name].update({1: self.g.get_vertex_property(2)['mstruct']}) + #: Whole-plant total structural mass (all axes' own shoot mstruct, plus the single shared + #: roots/grains which are only folded into the main stem's -- see + #: cnmetabolism.model.Axis.calculate_aggregated_variables). Reading only vertex 2 (MS) would + #: silently drop every explicit tiller's own real shoot mstruct; this reduces to exactly + #: today's MS-only value when tiller axes carry no 'mstruct' (i.e. explicit_tillers=False). + self.props[name].update({1: sum(self.g.get_vertex_property(v).get('mstruct', 0.0) for v in self.g.vertices(scale=2))}) elif name == "amino_acids_phloem": self.props[name].update({1: self.g.get_vertex_property(2)['phloem']['amino_acids']}) @@ -667,7 +685,7 @@ def __call__(self): # run hydraulics if self.hydraulics: if self.cnwgrass_roots: - turgor_soil = hydraulics_facade_.soils[(1, 'MS')] + turgor_soil = self.hydraulics_facade_.soils[(1, 'MS')] # Trigger drought if drought_trigger is not None and 'green_area' in drought_trigger.keys(): if (sum(g.property('green_area').values()) >= drought_trigger['green_area'] or drought_ongoing) and not drought_passed: @@ -738,7 +756,7 @@ def __call__(self): def scenario_utility(time_step_in_seconds: int = 3600, INPUTS_DIRPATH = "inputs", OUTPUTS_DIRPATH = "outputs", METEO_FILENAME = "meteo_Ljutovac2002.csv", plant_density = {1:250}, - forced_start_time = 0, tillers_replications={'T1': 0.5, 'T2': 0.5, 'T3': 0.5, 'T4': 0.5}, N_fertilizations = {2016: 357143, 2520: 1000000}, + forced_start_time = 0, tillers_replications={'T1': 0.5, 'T2': 0.5, 'T3': 0.5, 'T4': 0.5}, explicit_tillers=False, N_fertilizations = {2016: 357143, 2520: 1000000}, stored_times = None, option_static = False, single_plant = False, show_3Dplant = False, run_from_outputs = False, heterogeneous_canopy = True, update_parameters_all_models = None, isolated_roots = False, cnwgrass_roots = True, hydraulics = False): scenario = {} @@ -847,6 +865,7 @@ def scenario_utility(time_step_in_seconds: int = 3600, INPUTS_DIRPATH = "inputs" scenario["option_static"] = option_static scenario["show_3Dplant"] = show_3Dplant scenario["tillers_replications"] = tillers_replications + scenario["explicit_tillers"] = explicit_tillers scenario["heterogeneous_canopy"] = heterogeneous_canopy scenario["N_fertilizations"] = N_fertilizations scenario["update_parameters_all_models"] = update_parameters_all_models diff --git a/src/openalea/cnwgrass/integration/gasexchange_facade.py b/src/openalea/cnwgrass/integration/gasexchange_facade.py index 1e7b6cc..cc0a12c 100644 --- a/src/openalea/cnwgrass/integration/gasexchange_facade.py +++ b/src/openalea/cnwgrass/integration/gasexchange_facade.py @@ -47,6 +47,7 @@ def __init__(self, shared_mtg, shared_elements_inputs_outputs_df, stomatal_model_name='BWB', hydraulics=False, + explicit_tillers=False, update_parameters=None, update_shared_df=True): """ @@ -56,10 +57,12 @@ def __init__(self, shared_mtg, :param pandas.DataFrame shared_elements_inputs_outputs_df: the dataframe of inputs and outputs at elements scale shared between all models. :param str stomatal_model_name: the model of stomatal conductance. Should be one of 'BWB', 'Leuning', 'Tuzet' or 'hydraulics'. :param bool hydraulics: if True the model will assume the coupling to the turgor-driven growth model. + :param bool explicit_tillers: if True, compute real photosynthesis/transpiration/stomatal conductance for every tiller element found in the MTG, instead of only the main stem's. :param None or dict update_parameters: A dictionary with the parameters to update, should have the form {'param1': value1, 'param2': value2, ...}. :param bool update_shared_df: If `True` update the shared dataframes at init and at each run (unless stated otherwise) """ self._shared_mtg = shared_mtg #: the MTG shared between all models + self.explicit_tillers = explicit_tillers self._simulation = simulation.Simulation(update_parameters=update_parameters, stomatal_model_name=stomatal_model_name, hydraulics=hydraulics) #: the simulator to use to run the model @@ -81,7 +84,7 @@ def run(self, Ta, ambient_CO2, RH, Ur, update_shared_df=None): Run the model and update the MTG and the dataframes shared between all models. :param float Ta: air temperature at t (degree Celsius) - :param float ambient_CO2: air CO2 at t (µmol mol-1) + :param float ambient_CO2: air CO2 at t (�mol mol-1) :param float RH: relative humidity at t (decimal fraction) :param float Ur: wind speed at the top of the canopy at t (m s-1) :param bool update_shared_df: if 'True', update the shared dataframes at this time step. @@ -106,7 +109,7 @@ def _initialize_model(self): mtg_plant_index = int(self._shared_mtg.index(mtg_plant_vid)) for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): mtg_axis_label = self._shared_mtg.label(mtg_axis_vid) - if mtg_axis_label != 'MS': + if mtg_axis_label != 'MS' and not self.explicit_tillers: continue axis_id = (mtg_plant_index, mtg_axis_label) gasexchange_axis_inputs_dict = {} @@ -152,7 +155,11 @@ def _initialize_model(self): triangle_heights = get_height({mtg_element_vid: self._shared_mtg.property('geometry')[mtg_element_vid]}) mtg_element_input = np.nanmean(triangle_heights[mtg_element_vid]) else: - mtg_element_input = 0 + #: too small to have a geometry yet -- NaN (not 0) so run() treats it like a + #: HiddenElement (no photosynthesis calculation), consistent with the comment + #: in gasexchange/simulation.py's run(). A literal 0 was feeding Zh=0 into + #: organ_temperature(), causing a divide-by-zero. + mtg_element_input = np.nan height_element_list.append(mtg_element_input) #: Width is actually diameter for Sheath and Internodes if mtg_organ_label in ['sheath', 'internode', 'pedoncule', 'ear'] and gasexchange_element_input_name == 'width': diff --git a/src/openalea/cnwgrass/integration/growth_facade.py b/src/openalea/cnwgrass/integration/growth_facade.py index c2db35f..46cfe8c 100644 --- a/src/openalea/cnwgrass/integration/growth_facade.py +++ b/src/openalea/cnwgrass/integration/growth_facade.py @@ -41,6 +41,18 @@ class GrowthFacade(object): """ + #: hiddenzone-scale inputs declared as required by the Growth model but not unconditionally needed: + #: 'leaf_Lmax'/'internode_Lmax' are never actually read by any computation in growth/simulation.py; + #: 'is_over' is only ever written by Growth, never read as an input; 'delta_internode_pseudo_age' / + #: 'delta_leaf_pseudo_age' are declared in morphogenesis's own `hiddenzone_outputs` (written) but NOT in its + #: `hiddenzone_inputs` (read from the MTG) -- they only exist in the MTG once morphogenesis's real, + #: MS-only computation has freshly added them that same time step (internode past ligulation / leaf past + #: emergence), which is also exactly the only case Growth itself ever reads them (see the + #: `internode_pseudo_age >= internode_rapid_growth_t` / `leaf_is_growing` gates in growth/simulation.py) -- + #: so requiring them here before that state is ever reached would permanently exclude the hiddenzone. + #: See the note where this is used in `_initialize_model`. + HIDDENZONE_INPUTS_NOT_REQUIRED = {'leaf_Lmax', 'internode_Lmax', 'is_over', 'delta_internode_pseudo_age', 'delta_leaf_pseudo_age'} + def __init__(self, shared_mtg, delta_t, model_hiddenzones_inputs_df, model_elements_inputs_df, @@ -51,6 +63,7 @@ def __init__(self, shared_mtg, delta_t, shared_elements_inputs_outputs_df, shared_axes_inputs_outputs_df, hydraulics=False, + explicit_tillers=False, update_parameters=None, update_shared_df=True, cnwgrass_roots=True): @@ -67,11 +80,13 @@ def __init__(self, shared_mtg, delta_t, :param pandas.DataFrame shared_elements_inputs_outputs_df: the dataframe of inputs and outputs at elements scale shared between all models. :param pandas.DataFrame shared_axes_inputs_outputs_df: the dataframe of inputs and outputs at axis scale shared between all models. :param bool hydraulics: if True the model will assume the coupling to the turgor-driven growth model + :param bool explicit_tillers: if True, compute real dimensional/mass growth for every tiller's hiddenzones/elements found in the MTG, instead of only the main stem's. :param dict or None update_parameters: A dictionary with the parameters to update, should have the form {'param1': value1, 'param2': value2, ...}. :param bool update_shared_df: If `True` update the shared dataframes at init and at each run (unless stated otherwise) """ self._shared_mtg = shared_mtg #: the MTG shared between all models + self.explicit_tillers = explicit_tillers self._simulation = simulation.Simulation(delta_t=delta_t, hydraulics=hydraulics, update_parameters=update_parameters, cnwgrass_roots=cnwgrass_roots) #: the simulator to use to run the model @@ -122,7 +137,13 @@ def _initialize_model(self): mtg_plant_index = int(self._shared_mtg.index(mtg_plant_vid)) for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): mtg_axis_label = self._shared_mtg.label(mtg_axis_vid) - if mtg_axis_label != 'MS': + if isinstance(mtg_axis_label, bytes): + #: Some MTGs carry byte-string axis labels (numpy.bytes_ included) -- normalize to str so + #: 'MS' comparisons behave consistently, and so hiddenzone_id/element_id tuples built from it + #: don't mix str and bytes axis labels within the same `sorted(...)` call in growth/simulation.py + #: (str/bytes are not orderable against each other and sorted() raises TypeError otherwise). + mtg_axis_label = mtg_axis_label.decode('UTF-8') + if mtg_axis_label != 'MS' and not self.explicit_tillers: continue mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) @@ -160,10 +181,23 @@ def _initialize_model(self): hiddenzone_id = (mtg_plant_index, mtg_axis_label, mtg_metamer_index) mtg_hiddenzone_properties = mtg_metamer_properties['hiddenzone'] - if set(mtg_hiddenzone_properties).issuperset(self._simulation.hiddenzone_inputs): # Initial values are set by morphogenesis + #: 'leaf_Lmax' / 'internode_Lmax' are declared as required hiddenzone inputs but never + #: actually read by any computation in growth/simulation.py, and 'is_over' is Growth's + #: own output (only ever written, never read as an input) -- so none of the three need to + #: already be present in the MTG. This matters for a brand-new tiller hiddenzone: under + #: hydraulics=True, morphogenesis itself stops tracking 'leaf_Lmax' (see + #: morphogenesis/model.py's `variables_to_delete`), so a tiller can never inherit it from + #: MS via the copy mechanism, and 'is_over' is never set until Growth itself first + #: processes the hiddenzone -- requiring them here would permanently exclude every tiller. + required_hiddenzone_inputs = set(self._simulation.hiddenzone_inputs) - GrowthFacade.HIDDENZONE_INPUTS_NOT_REQUIRED + if set(mtg_hiddenzone_properties).issuperset(required_hiddenzone_inputs): # Initial values are set by morphogenesis growth_hiddenzone_inputs_dict = {} for growth_hiddenzone_input_name in self._simulation.hiddenzone_inputs: - growth_hiddenzone_inputs_dict[growth_hiddenzone_input_name] = mtg_hiddenzone_properties[growth_hiddenzone_input_name] + #: default to 0.0 (not None) when absent: 'delta_internode_pseudo_age' / 'delta_leaf_pseudo_age' + #: can genuinely be read by a numeric computation once the corresponding hiddenzone + #: reaches the relevant developmental stage (see HIDDENZONE_INPUTS_NOT_REQUIRED above); + #: 0.0 ("no progress this step") is a safe fallback there, whereas None would raise. + growth_hiddenzone_inputs_dict[growth_hiddenzone_input_name] = mtg_hiddenzone_properties.get(growth_hiddenzone_input_name, 0.0) all_growth_hiddenzones_inputs_dict[hiddenzone_id] = growth_hiddenzone_inputs_dict # We take only the elements of growing metamers i.e. the ones with hiddenzones @@ -224,9 +258,18 @@ def _update_shared_MTG(self, all_growth_hiddenzones_data_dict, all_growth_elemen mtg_plant_index = int(self._shared_mtg.index(mtg_plant_vid)) for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): mtg_axis_label = self._shared_mtg.label(mtg_axis_vid) + if isinstance(mtg_axis_label, bytes): + mtg_axis_label = mtg_axis_label.decode('UTF-8') axis_id = (mtg_plant_index, mtg_axis_label) - if mtg_axis_label != 'MS': + if mtg_axis_label != 'MS' and not self.explicit_tillers: + continue + + if axis_id not in all_growth_axes_data_dict: + #: A tiller freshly created by morphogenesis this same time step, or otherwise not yet + #: carrying the axis-scale inputs Growth requires, was excluded from _initialize_model's + #: inputs (see the `set(...).issuperset(...)` check above) and so has no output either -- + #: skip it for this time step rather than crashing on a missing dict entry. continue growth_axis_data_dict = all_growth_axes_data_dict[axis_id] @@ -254,8 +297,16 @@ def _update_shared_MTG(self, all_growth_hiddenzones_data_dict, all_growth_elemen for hiddenzone_data_name, hiddenzone_data_value in growth_hiddenzone_data_dict.items(): self._shared_mtg.property('hiddenzone')[mtg_metamer_vid][hiddenzone_data_name] = hiddenzone_data_value - elif 'hiddenzone' in self._shared_mtg.get_vertex_property(mtg_metamer_vid): - # remove the 'hiddenzone' property from this metamer + elif mtg_axis_label == 'MS' and 'hiddenzone' in self._shared_mtg.get_vertex_property(mtg_metamer_vid): + #: remove the 'hiddenzone' property from this metamer -- only valid for the main stem, + #: whose hiddenzones are self-sustained by Growth's own writes every time step (see the + #: `issuperset` check in _initialize_model): there, hiddenzone_id being absent from + #: Growth's outputs really does mean this leaf has fully emerged and its hiddenzone is + #: legitimately gone. A tiller's hiddenzone is never self-sustained that way (it is + #: seeded from morphogenesis's copy-from-MS mechanism, not from Growth), so its + #: hiddenzone_id can be absent from Growth's outputs simply because Growth doesn't have + #: all the inputs it wants for it yet -- deleting it in that case would silently wipe + #: the tiller's own geometry, not reflect a real developmental event. del self._shared_mtg.property('hiddenzone')[mtg_metamer_vid] #: Organ scale diff --git a/src/openalea/cnwgrass/integration/hydraulics_facade.py b/src/openalea/cnwgrass/integration/hydraulics_facade.py index 42ae327..51c2af1 100644 --- a/src/openalea/cnwgrass/integration/hydraulics_facade.py +++ b/src/openalea/cnwgrass/integration/hydraulics_facade.py @@ -1,5 +1,6 @@ # -*- coding: latin-1 -*- import math +from scipy.sparse import lil_matrix from openalea.cnwgrass.hydraulics import simulation as hydraulics_simulation, \ postprocessing as hydraulics_postprocessing @@ -76,7 +77,8 @@ def __init__(self, shared_mtg, delta_t, update_parameters, shared_soils_inputs_outputs_df, update_shared_df=True, isolated_roots=False, - cnwgrass_roots=True): + cnwgrass_roots=True, + explicit_tillers=False): """ :param openalea.mtg.mtg.MTG shared_mtg: The MTG shared between all models. :param int delta_t: The delta between two runs, in seconds. @@ -90,11 +92,15 @@ def __init__(self, shared_mtg, delta_t, update_parameters, :param pandas.DataFrame shared_organs_inputs_outputs_df: the dataframe of inputs and outputs at organ scale shared between all models. :param pandas.DataFrame shared_soils_inputs_outputs_df: the dataframe of inputs and outputs at soils scale shared between all models. :param bool update_shared_df: If `True` update the shared dataframes at init and at each run (unless stated otherwise) + :param bool explicit_tillers: if True, build and integrate a real Axis for every tiller found in the MTG (sharing the main stem's roots/xylem) instead of ignoring + every axis except the main stem. """ - + self._shared_mtg = shared_mtg #: the MTG shared between all models - self._simulation = hydraulics_simulation.Simulation(delta_t=delta_t, isolated_roots=isolated_roots, cnwgrass_roots=cnwgrass_roots) + self.explicit_tillers = explicit_tillers + + self._simulation = hydraulics_simulation.Simulation(delta_t=delta_t, isolated_roots=isolated_roots, cnwgrass_roots=cnwgrass_roots, explicit_tillers=explicit_tillers) self.population, self.soils = hydraulics_converter.from_dataframes(model_axes_inputs_df, model_hiddenzones_inputs_df, model_elements_inputs_df, model_organs_inputs_df, model_soils_inputs_df) @@ -126,6 +132,10 @@ def run(self, update_shared_df=False): """ self._initialize_model() + if self.isolated_roots and not self._simulation.cnwgrass_roots: + #: Only this (isolated_roots=True, cnwgrass_roots=False) branch's solve_ivp call actually uses + #: jac_sparsity -- see _initialize_indices_and_sparsity's docstring for why this is safe. + self._initialize_indices_and_sparsity() self._simulation.run() self._update_shared_MTG() @@ -193,13 +203,243 @@ def graphs(axes_postprocessing_df, hiddenzones_postprocessing_df, elements_postp hydraulics_postprocessing.generate_graphs(axes_df=axes_postprocessing_df, hiddenzones_df=hiddenzones_postprocessing_df, elements_df=elements_postprocessing_df, organs_df=organs_postprocessing_df, soils_df=soils_postprocessing_df, meteo_data=meteo_data, graphs_dirpath=graphs_dirpath) + @staticmethod + def _is_unusable_mtg_value(value): + """A property value counts as unusable (i.e. not actually available yet, even though its key is + present) if it's missing (None) or a non-finite number (NaN/inf) -- e.g. a seed CSV placeholder for a + hiddenzone/element that hasn't reached this stage of growth yet (only the currently-most-advanced + hiddenzone/element on each axis gets a real seeded water_content/width/thickness/turgor_water_potential; + every other row that's still to come has the column present but NaN). Used by the "first time this + object passes into hydraulics" exemptions below, which otherwise only checked for the KEY being absent + -- a present-but-NaN value would pass that check, get copied verbatim onto the model object, and crash + solve_ivp's y0 finiteness check, even though HiddenZone/PhotosyntheticOrganElement's own class defaults + (INIT_COMPARTMENTS) are already sensible, finite seed values for exactly this situation.""" + if value is None: + return True + if isinstance(value, (int, float)) and not math.isfinite(value): + return True + return False + + def _build_axis_organs(self, mtg_axis_vid, mtg_axis_label): + """Build the Axis and its Roots/Xylem organs. Only ever called for MS -- roots/xylem are the single + below-ground root system and xylem network of the whole plant, shared by reference with every + explicit tiller axis (see the second pass in :meth:`_initialize_model`).""" + hydraulics_axis = hydraulics_model.Axis(mtg_axis_label) + mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) + hydraulics_axis_data_dict = {} + for hydraulics_axis_data_name in hydraulics_simulation.Simulation.AXES_STATE: + hydraulics_axis_data_dict[hydraulics_axis_data_name] = mtg_axis_properties[hydraulics_axis_data_name] + hydraulics_axis.__dict__.update(hydraulics_axis_data_dict) + for hydraulics_organ_class in (hydraulics_model.Roots, hydraulics_model.Xylem): + mtg_organ_label = hydraulics_converter.hydraulics_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[hydraulics_organ_class] + # create a new organ + hydraulics_organ = hydraulics_organ_class(mtg_organ_label) + mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) + if mtg_organ_label in mtg_axis_properties: + mtg_organ_properties = mtg_axis_properties[mtg_organ_label] + access_mtg_names = hydraulics_simulation.Simulation.ORGANS_STATE + if hydraulics_organ_class == hydraulics_model.Xylem and self.isolated_roots: + access_mtg_names += ['water_potential', 'root_xylem_water_potential', 'shoot_root_xylem_conductance'] + hydraulics_organ_data_names = set(access_mtg_names).intersection(hydraulics_organ.__dict__) + if set(mtg_organ_properties).issuperset(hydraulics_organ_data_names): + hydraulics_organ_data_dict = {} + for hydraulics_organ_data_name in hydraulics_organ_data_names: + hydraulics_organ_data_dict[hydraulics_organ_data_name] = mtg_organ_properties[hydraulics_organ_data_name] + + # Debug: Tell if missing input variable + if math.isnan(mtg_organ_properties[hydraulics_organ_data_name]) or mtg_organ_properties[hydraulics_organ_data_name] is None: + print('Missing variable', hydraulics_organ_data_name, 'for vertex id', mtg_axis_vid, 'which is', mtg_organ_label) + + hydraulics_organ.__dict__.update(hydraulics_organ_data_dict) + + # Update parameters if specified + if mtg_organ_label in self._update_parameters: + hydraulics_organ.PARAMETERS.__dict__.update(self._update_parameters[mtg_organ_label]) + + hydraulics_organ.initialize() + # add the new organ to current axis + setattr(hydraulics_axis, mtg_organ_label, hydraulics_organ) + return hydraulics_axis + + def _build_axis_phytomers(self, mtg_axis_vid, hydraulics_axis): + """Build the phytomers of `hydraulics_axis` from the MTG. Returns whether at least one valid phytomer was found.""" + has_valid_phytomer = False + for mtg_metamer_vid in self._shared_mtg.components_iter(mtg_axis_vid): + mtg_metamer_index = int(self._shared_mtg.index(mtg_metamer_vid)) + + # create a new phytomer + hydraulics_phytomer = hydraulics_model.Phytomer(mtg_metamer_index) + mtg_hiddenzone_label = hydraulics_converter.hydraulics_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[hydraulics_model.HiddenZone] + mtg_metamer_properties = self._shared_mtg.get_vertex_property(mtg_metamer_vid) + + if mtg_hiddenzone_label in mtg_metamer_properties and mtg_metamer_properties[mtg_hiddenzone_label]['leaf_is_growing']: + has_valid_hiddenzone = True + hydraulics_hiddenzone = hydraulics_model.HiddenZone(label=mtg_hiddenzone_label) + mtg_hiddenzone_properties = mtg_metamer_properties[mtg_hiddenzone_label] + + # Adding aggregated variables into inputs + hydraulics_hiddenzone_data_names = set(hydraulics_simulation.Simulation.HIDDENZONE_RUN_VARIABLES).intersection(hydraulics_hiddenzone.__dict__) + + #: A hiddenzone whose leaf_pseudo_age is already > 0 (i.e. it emerged/started growing before + #: hydraulics ever tracked it -- true for a tiller's currently-most-advanced hiddenzone the + #: first time explicit_tillers activates hydraulics for it) can still be missing individual + #: fields the old MS-only seed data never bothered to fill in (e.g. leaf_Wmax), even though + #: most of its other fields are genuinely seeded. leaf_pseudo_age == 0 was only ever an + #: imperfect proxy for "not seeded yet" -- apply the missing-or-unusable exemption + #: unconditionally instead, so any such gap falls back to the class's own default rather than + #: silently letting a present-but-NaN MTG value overwrite it. + missing_initial_hiddenzone_properties = { + name for name in hydraulics_hiddenzone_data_names + if name not in mtg_hiddenzone_properties or self._is_unusable_mtg_value(mtg_hiddenzone_properties[name]) + } + hydraulics_hiddenzone_data_names -= missing_initial_hiddenzone_properties + + if set(mtg_hiddenzone_properties).issuperset(hydraulics_hiddenzone_data_names): + hydraulics_hiddenzone_data_dict = {} + for hydraulics_hiddenzone_data_name in hydraulics_hiddenzone_data_names: + mtg_hiddenzone_data_value = mtg_hiddenzone_properties.get(hydraulics_hiddenzone_data_name) + hydraulics_hiddenzone_data_dict[hydraulics_hiddenzone_data_name] = mtg_hiddenzone_data_value + hydraulics_hiddenzone.__dict__.update(hydraulics_hiddenzone_data_dict) + + # Update parameters if specified + if mtg_hiddenzone_label in self._update_parameters: + hydraulics_hiddenzone.PARAMETERS.__dict__.update(self._update_parameters[mtg_hiddenzone_label]) + + hydraulics_hiddenzone.initialize() + + #: leaf_Wmax/lamina_Lmax default to None (not a usable finite value like water_content/width/ + #: thickness/turgor_water_potential do) -- if either is still unusable at this point (missing + #: from the MTG or exempted above), seed it the same way the model itself does the first time + #: it computes a hiddenzone's dimensions (see hydraulics/simulation.py's leaf_pseudo_age == 0 + #: branch: leaf_Wmax = width, and leaf_Lmax = leaf_L is set unconditionally for every + #: hiddenzone), instead of leaving None to blow up a later `+=`. + if self._is_unusable_mtg_value(hydraulics_hiddenzone.leaf_Wmax): + hydraulics_hiddenzone.leaf_Wmax = hydraulics_hiddenzone.width + if self._is_unusable_mtg_value(hydraulics_hiddenzone.lamina_Lmax): + hydraulics_hiddenzone.lamina_Lmax = hydraulics_hiddenzone.leaf_L + + # add the new hiddenzone to current phytomer + setattr(hydraulics_phytomer, mtg_hiddenzone_label, hydraulics_hiddenzone) + + else: + has_valid_hiddenzone = False + + # create a new organ + has_valid_organ = False + for mtg_organ_vid in self._shared_mtg.components_iter(mtg_metamer_vid): + mtg_organ_label = self._shared_mtg.label(mtg_organ_vid) + if mtg_organ_label == "internode": # No internode in Hydraulics model + continue + if mtg_organ_label not in MTG_TO_hydraulics_PHYTOMERS_ORGANS_MAPPING or self._shared_mtg.get_vertex_property(mtg_organ_vid)['length'] == 0: + continue + hydraulics_organ_class = MTG_TO_hydraulics_PHYTOMERS_ORGANS_MAPPING[mtg_organ_label] + hydraulics_organ = hydraulics_organ_class(mtg_organ_label) + + # Update parameters if specified + if 'PhotosyntheticOrgan' in self._update_parameters: + hydraulics_organ.PARAMETERS.__dict__.update(self._update_parameters['PhotosyntheticOrgan']) + + hydraulics_organ.initialize() + has_valid_element = False + + # create a new element + for mtg_element_vid in self._shared_mtg.components_iter(mtg_organ_vid): + mtg_element_label = self._shared_mtg.label(mtg_element_vid) + if mtg_element_label not in hydraulics_converter.DATAFRAME_TO_hydraulics_ELEMENTS_NAMES_MAPPING \ + or (self._shared_mtg.get_vertex_property(mtg_element_vid)['length'] == 0) \ + or ((mtg_element_label == 'HiddenElement') and (self._shared_mtg.get_vertex_property(mtg_element_vid).get('is_growing', True)) \ + or (self._shared_mtg.get_vertex_property(mtg_element_vid).get('is_over', True))): + continue + + has_valid_element = True + hydraulics_element = hydraulics_ORGANS_TO_ELEMENTS_MAPPING[hydraulics_organ_class](label=mtg_element_label) + mtg_element_properties = self._shared_mtg.get_vertex_property(mtg_element_vid) + hydraulics_element_data_names = set(hydraulics_simulation.Simulation.ELEMENTS_RUN_VARIABLES).intersection(hydraulics_element.__dict__) #: Adding aggregated variables into inputs + #: See the equivalent hiddenzone comment above -- age == 0 was only ever an imperfect proxy + #: for "not seeded yet", and fails the same way for a tiller element that's already past + #: its own age == 0 moment by the time hydraulics first tracks it. Apply unconditionally. + missing_initial_element_properties = { + name for name in hydraulics_element_data_names + if name not in mtg_element_properties or self._is_unusable_mtg_value(mtg_element_properties[name]) + } + hydraulics_element_data_names -= missing_initial_element_properties + + if set(mtg_element_properties).issuperset(hydraulics_element_data_names): + hydraulics_element_data_dict = {} + for hydraulics_element_data_name in hydraulics_element_data_names: + mtg_element_data_value = mtg_element_properties.get(hydraulics_element_data_name) + hydraulics_element_data_dict[hydraulics_element_data_name] = mtg_element_data_value + hydraulics_element.__dict__.update(hydraulics_element_data_dict) + # add element to organ + setattr(hydraulics_organ, hydraulics_converter.DATAFRAME_TO_hydraulics_ELEMENTS_NAMES_MAPPING[mtg_element_label], hydraulics_element) + + #: Update lamina_Lmax & Wmax + if mtg_organ_label == 'blade': + mtg_organ_properties = self._shared_mtg.get_vertex_property(mtg_organ_vid) + if has_valid_hiddenzone is True: + if mtg_element_properties['length'] >= mtg_hiddenzone_properties['lamina_Lmax']: + mtg_hiddenzone_properties['lamina_Lmax'] = mtg_element_properties['length'] + mtg_element_properties['Wmax'] = mtg_hiddenzone_properties['leaf_Wmax'] + mtg_organ_properties['shape_max_width'] = mtg_hiddenzone_properties['leaf_Wmax'] + mtg_organ_properties['shape_mature_length'] = mtg_hiddenzone_properties['lamina_Lmax'] + else: + mtg_organ_properties['shape_max_width'] = mtg_element_properties['Wmax'] + mtg_organ_properties['shape_mature_length'] = mtg_element_properties['length'] + + if has_valid_element: + has_valid_organ = True + setattr(hydraulics_phytomer, hydraulics_ATTRIBUTES_MAPPING[hydraulics_organ_class], hydraulics_organ) + + if has_valid_organ or has_valid_hiddenzone: + hydraulics_axis.phytomers.append(hydraulics_phytomer) + has_valid_phytomer = True + + return has_valid_phytomer + + def _initialize_indices_and_sparsity(self): + """Build the sparse Jacobian pattern for the shoot ODE system (_calculate_shoot_derivatives). + + Every hiddenzone's and every element's derivatives depend only on its own state, plus + axis.xylem.water_potential and axis.SAM_temperature -- + both fixed, externally-set parameters during this solve, never written inside it, so they contribute + no Jacobian columns. The one genuine coupling is *within a single phytomer*: a blade/sheath element's + age == 0 bootstrap inherits its thickness/width from its own hiddenzone (lines ~1207-1209), and once + growing, the hiddenzone's leaf_L row and the element's length/width/thickness rows cross-reference + each other's delta_*_dimensions terms (lines ~1361-1367, 1376). There is no dependency at all across + different phytomers, metamers, or axes -- everything else routes only through the constant xylem. + So the true Jacobian is block-diagonal, one dense block per phytomer (hiddenzone + its own elements), + with nothing connecting different blocks. This is rebuilt every call since the state layout (and which + phytomers/elements exist) can change every timestep. + """ + n = len(self._simulation.initial_conditions) + S = lil_matrix((n, n), dtype=bool) + S.setdiag(True) + + icm = self._simulation.initial_conditions_mapping + for plant in self.population.plants: + for axis in plant.axes: + for phytomer in axis.phytomers: + block_idxs = [] + if phytomer.hiddenzone is not None and phytomer.hiddenzone in icm: + block_idxs.extend(icm[phytomer.hiddenzone].values()) + 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 and element in icm: + block_idxs.extend(icm[element].values()) + for i in block_idxs: + S.rows[i].extend(block_idxs) + S.data[i].extend([True] * len(block_idxs)) + + self._simulation._jac_sparsity_shoot = S.tocsr() + def _initialize_model(self): """ Initialize the inputs of the model from the MTG shared between all models. """ self.population = hydraulics_model.Population() - mapping_topology = {'predecessor': {}, 'successor': {}} # traverse the MTG recursively from top @@ -209,163 +449,50 @@ def _initialize_model(self): # create a new plant hydraulics_plant = hydraulics_model.Plant(mtg_plant_index) is_valid_plant = False + ms_axis = None + + #: First pass: MS, which owns the roots/xylem organs shared by every explicit tiller axis. for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): mtg_axis_label = self._shared_mtg.label(mtg_axis_vid) - #: keep only MS TODO: temporary + if isinstance(mtg_axis_label, bytes): + mtg_axis_label = mtg_axis_label.decode('UTF-8') if mtg_axis_label != 'MS': continue - #: MS - # create a new axis - hydraulics_axis = hydraulics_model.Axis(mtg_axis_label) - mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) - hydraulics_axis_data_dict = {} - for hydraulics_axis_data_name in hydraulics_simulation.Simulation.AXES_STATE: - hydraulics_axis_data_dict[hydraulics_axis_data_name] = mtg_axis_properties[hydraulics_axis_data_name] - hydraulics_axis.__dict__.update(hydraulics_axis_data_dict) - is_valid_axis = True - for hydraulics_organ_class in (hydraulics_model.Roots, hydraulics_model.Xylem): - mtg_organ_label = hydraulics_converter.hydraulics_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[hydraulics_organ_class] - # create a new organ - hydraulics_organ = hydraulics_organ_class(mtg_organ_label) - mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) - if mtg_organ_label in mtg_axis_properties: - mtg_organ_properties = mtg_axis_properties[mtg_organ_label] - access_mtg_names = hydraulics_simulation.Simulation.ORGANS_STATE - if hydraulics_organ_class == hydraulics_model.Xylem and self.isolated_roots: - access_mtg_names += ['water_potential', 'root_xylem_water_potential', 'shoot_root_xylem_conductance'] - hydraulics_organ_data_names = set(access_mtg_names).intersection(hydraulics_organ.__dict__) - if set(mtg_organ_properties).issuperset(hydraulics_organ_data_names): - hydraulics_organ_data_dict = {} - for hydraulics_organ_data_name in hydraulics_organ_data_names: - hydraulics_organ_data_dict[hydraulics_organ_data_name] = mtg_organ_properties[hydraulics_organ_data_name] - - # Debug: Tell if missing input variable - if math.isnan(mtg_organ_properties[hydraulics_organ_data_name]) or mtg_organ_properties[hydraulics_organ_data_name] is None: - print('Missing variable', hydraulics_organ_data_name, 'for vertex id', mtg_axis_vid, 'which is', mtg_organ_label) - - hydraulics_organ.__dict__.update(hydraulics_organ_data_dict) - - # Update parameters if specified - if mtg_organ_label in self._update_parameters: - hydraulics_organ.PARAMETERS.__dict__.update(self._update_parameters[mtg_organ_label]) - - hydraulics_organ.initialize() - # add the new organ to current axis - setattr(hydraulics_axis, mtg_organ_label, hydraulics_organ) - if not is_valid_axis: - continue - - has_valid_phytomer = False - for mtg_metamer_vid in self._shared_mtg.components_iter(mtg_axis_vid): - mtg_metamer_index = int(self._shared_mtg.index(mtg_metamer_vid)) - - # create a new phytomer - hydraulics_phytomer = hydraulics_model.Phytomer(mtg_metamer_index) - mtg_hiddenzone_label = hydraulics_converter.hydraulics_CLASSES_TO_DATAFRAME_ORGANS_MAPPING[hydraulics_model.HiddenZone] - mtg_metamer_properties = self._shared_mtg.get_vertex_property(mtg_metamer_vid) - - if mtg_hiddenzone_label in mtg_metamer_properties and mtg_metamer_properties[mtg_hiddenzone_label]['leaf_is_growing']: - has_valid_hiddenzone = True - hydraulics_hiddenzone = hydraulics_model.HiddenZone(label=mtg_hiddenzone_label) - mtg_hiddenzone_properties = mtg_metamer_properties[mtg_hiddenzone_label] - - # Adding aggregated variables into inputs - hydraulics_hiddenzone_data_names = set(hydraulics_simulation.Simulation.HIDDENZONE_RUN_VARIABLES).intersection(hydraulics_hiddenzone.__dict__) - - if mtg_hiddenzone_properties.get('leaf_pseudo_age') == 0: # First time hiddenzone passes into hydraulics sub-model - missing_initial_hiddenzone_properties = hydraulics_hiddenzone_data_names - set(mtg_hiddenzone_properties) - hydraulics_hiddenzone_data_names -= missing_initial_hiddenzone_properties + hydraulics_axis = self._build_axis_organs(mtg_axis_vid, mtg_axis_label) + has_valid_phytomer = self._build_axis_phytomers(mtg_axis_vid, hydraulics_axis) - if set(mtg_hiddenzone_properties).issuperset(hydraulics_hiddenzone_data_names): - hydraulics_hiddenzone_data_dict = {} - for hydraulics_hiddenzone_data_name in hydraulics_hiddenzone_data_names: - mtg_hiddenzone_data_value = mtg_hiddenzone_properties.get(hydraulics_hiddenzone_data_name) - hydraulics_hiddenzone_data_dict[hydraulics_hiddenzone_data_name] = mtg_hiddenzone_data_value - hydraulics_hiddenzone.__dict__.update(hydraulics_hiddenzone_data_dict) - - # Update parameters if specified - if mtg_hiddenzone_label in self._update_parameters: - hydraulics_hiddenzone.PARAMETERS.__dict__.update(self._update_parameters[mtg_hiddenzone_label]) - - hydraulics_hiddenzone.initialize() - # add the new hiddenzone to current phytomer - setattr(hydraulics_phytomer, mtg_hiddenzone_label, hydraulics_hiddenzone) - - else: - has_valid_hiddenzone = False - - # create a new organ - has_valid_organ = False - for mtg_organ_vid in self._shared_mtg.components_iter(mtg_metamer_vid): - mtg_organ_label = self._shared_mtg.label(mtg_organ_vid) - if mtg_organ_label == "internode": # No internode in Hydraulics model - continue - if mtg_organ_label not in MTG_TO_hydraulics_PHYTOMERS_ORGANS_MAPPING or self._shared_mtg.get_vertex_property(mtg_organ_vid)['length'] == 0: - continue - hydraulics_organ_class = MTG_TO_hydraulics_PHYTOMERS_ORGANS_MAPPING[mtg_organ_label] - hydraulics_organ = hydraulics_organ_class(mtg_organ_label) - - # Update parameters if specified - if 'PhotosyntheticOrgan' in self._update_parameters: - hydraulics_organ.PARAMETERS.__dict__.update(self._update_parameters['PhotosyntheticOrgan']) - - hydraulics_organ.initialize() - has_valid_element = False - - # create a new element - for mtg_element_vid in self._shared_mtg.components_iter(mtg_organ_vid): - mtg_element_label = self._shared_mtg.label(mtg_element_vid) - if mtg_element_label not in hydraulics_converter.DATAFRAME_TO_hydraulics_ELEMENTS_NAMES_MAPPING \ - or (self._shared_mtg.get_vertex_property(mtg_element_vid)['length'] == 0) \ - or ((mtg_element_label == 'HiddenElement') and (self._shared_mtg.get_vertex_property(mtg_element_vid).get('is_growing', True)) \ - or (self._shared_mtg.get_vertex_property(mtg_element_vid).get('is_over', True))): - continue - - has_valid_element = True - hydraulics_element = hydraulics_ORGANS_TO_ELEMENTS_MAPPING[hydraulics_organ_class](label=mtg_element_label) - mtg_element_properties = self._shared_mtg.get_vertex_property(mtg_element_vid) - hydraulics_element_data_names = set(hydraulics_simulation.Simulation.ELEMENTS_RUN_VARIABLES).intersection(hydraulics_element.__dict__) #: Adding aggregated variables into inputs - if mtg_element_properties.get('age') == 0: # First time element passes into hydraulics model - missing_initial_element_properties = hydraulics_element_data_names - set(mtg_element_properties) - hydraulics_element_data_names -= missing_initial_element_properties - - if set(mtg_element_properties).issuperset(hydraulics_element_data_names): - hydraulics_element_data_dict = {} - for hydraulics_element_data_name in hydraulics_element_data_names: - mtg_element_data_value = mtg_element_properties.get(hydraulics_element_data_name) - hydraulics_element_data_dict[hydraulics_element_data_name] = mtg_element_data_value - hydraulics_element.__dict__.update(hydraulics_element_data_dict) - # add element to organ - setattr(hydraulics_organ, hydraulics_converter.DATAFRAME_TO_hydraulics_ELEMENTS_NAMES_MAPPING[mtg_element_label], hydraulics_element) - - #: Update lamina_Lmax & Wmax - if mtg_organ_label == 'blade': - if has_valid_hiddenzone is True: - mtg_organ_properties = self._shared_mtg.get_vertex_property(mtg_organ_vid) - if mtg_element_properties['length'] >= mtg_hiddenzone_properties['lamina_Lmax']: - mtg_hiddenzone_properties['lamina_Lmax'] = mtg_element_properties['length'] - mtg_element_properties['Wmax'] = mtg_hiddenzone_properties['leaf_Wmax'] - mtg_organ_properties['shape_max_width'] = mtg_hiddenzone_properties['leaf_Wmax'] - mtg_organ_properties['shape_mature_length'] = mtg_hiddenzone_properties['lamina_Lmax'] - else: - mtg_organ_properties['shape_max_width'] = mtg_element_properties['Wmax'] - mtg_organ_properties['shape_mature_length'] = mtg_element_properties['length'] - - if has_valid_element: - has_valid_organ = True - setattr(hydraulics_phytomer, hydraulics_ATTRIBUTES_MAPPING[hydraulics_organ_class], hydraulics_organ) - - if has_valid_organ or has_valid_hiddenzone: - hydraulics_axis.phytomers.append(hydraulics_phytomer) - has_valid_phytomer = True - - if not has_valid_phytomer: - is_valid_axis = False - - if is_valid_axis: + if has_valid_phytomer: hydraulics_plant.axes.append(hydraulics_axis) is_valid_plant = True + ms_axis = hydraulics_axis + + #: Second pass: real tiller axes, sharing MS's roots/xylem -- there is no independent below-ground + #: root system or xylem network per tiller. + if self.explicit_tillers and ms_axis is not None: + for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): + mtg_axis_label = self._shared_mtg.label(mtg_axis_vid) + if isinstance(mtg_axis_label, bytes): + mtg_axis_label = mtg_axis_label.decode('UTF-8') + if mtg_axis_label == 'MS': + continue + + try: + hydraulics_tiller_axis = hydraulics_model.Axis(mtg_axis_label) + mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) + hydraulics_axis_data_dict = {} + for hydraulics_axis_data_name in hydraulics_simulation.Simulation.AXES_STATE: + hydraulics_axis_data_dict[hydraulics_axis_data_name] = mtg_axis_properties[hydraulics_axis_data_name] + hydraulics_tiller_axis.__dict__.update(hydraulics_axis_data_dict) + + hydraulics_tiller_axis.roots = ms_axis.roots + hydraulics_tiller_axis.xylem = ms_axis.xylem + except (KeyError, TypeError): + continue + + has_valid_phytomer = self._build_axis_phytomers(mtg_axis_vid, hydraulics_tiller_axis) + if has_valid_phytomer: + hydraulics_plant.axes.append(hydraulics_tiller_axis) if is_valid_plant: self.population.plants.append(hydraulics_plant) @@ -398,7 +525,10 @@ def _update_shared_MTG(self): hydraulics_axis_label = hydraulics_axis.label while True: mtg_axis_vid = next(mtg_axes_iterator) - if self._shared_mtg.label(mtg_axis_vid) == hydraulics_axis_label: + found_axis_label = self._shared_mtg.label(mtg_axis_vid) + if isinstance(found_axis_label, bytes): + found_axis_label = found_axis_label.decode('UTF-8') + if found_axis_label == hydraulics_axis_label: break # Xylem @@ -406,16 +536,20 @@ def _update_shared_MTG(self): for hydraulics_axis_property_name in hydraulics_axis_property_names: hydraulics_axis_property_value = getattr(hydraulics_axis, hydraulics_axis_property_name) self._shared_mtg.property(hydraulics_axis_property_name)[mtg_axis_vid] = hydraulics_axis_property_value - for mtg_organ_label in MTG_TO_hydraulics_AXES_ORGANS_MAPPING.keys(): - if mtg_organ_label not in self._shared_mtg.get_vertex_property(mtg_axis_vid): - # Add a property describing the organ to the current axis of the MTG - self._shared_mtg.property(mtg_organ_label)[mtg_axis_vid] = {} - # Update the property describing the organ of the current axis in the MTG - hydraulics_organ = getattr(hydraulics_axis, mtg_organ_label) - mtg_organ_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid)[mtg_organ_label] - for hydraulics_property_name in hydraulics_simulation.Simulation.ORGANS_RUN_VARIABLES: - if hasattr(hydraulics_organ, hydraulics_property_name): - mtg_organ_properties[hydraulics_property_name] = getattr(hydraulics_organ, hydraulics_property_name) + #: roots/xylem are shared by reference across every axis of the plant (see _build_axis_organs / + #: the explicit-tillers second pass in _initialize_model) -- write them back to the MTG only + #: once, under MS, so they are not duplicated (with byte-identical values) under every tiller axis. + if hydraulics_axis_label == 'MS': + for mtg_organ_label in MTG_TO_hydraulics_AXES_ORGANS_MAPPING.keys(): + if mtg_organ_label not in self._shared_mtg.get_vertex_property(mtg_axis_vid): + # Add a property describing the organ to the current axis of the MTG + self._shared_mtg.property(mtg_organ_label)[mtg_axis_vid] = {} + # Update the property describing the organ of the current axis in the MTG + hydraulics_organ = getattr(hydraulics_axis, mtg_organ_label) + mtg_organ_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid)[mtg_organ_label] + for hydraulics_property_name in hydraulics_simulation.Simulation.ORGANS_RUN_VARIABLES: + if hasattr(hydraulics_organ, hydraulics_property_name): + mtg_organ_properties[hydraulics_property_name] = getattr(hydraulics_organ, hydraulics_property_name) mtg_metamers_iterator = self._shared_mtg.components_iter(mtg_axis_vid) for hydraulics_phytomer in hydraulics_axis.phytomers: diff --git a/src/openalea/cnwgrass/integration/morphogenesis_facade.py b/src/openalea/cnwgrass/integration/morphogenesis_facade.py index 8a205c7..4803d33 100644 --- a/src/openalea/cnwgrass/integration/morphogenesis_facade.py +++ b/src/openalea/cnwgrass/integration/morphogenesis_facade.py @@ -47,7 +47,7 @@ def __init__(self, shared_mtg, delta_t, shared_elements_inputs_outputs_df, geometrical_model, phytoT=None, hydraulics=False, optimal_growth_option=False, option_static=False, - update_parameters=None, update_shared_df=True): + update_parameters=None, update_shared_df=True, explicit_tillers=False): """ :param openalea.mtg.mtg.MTG shared_mtg: The MTG shared between all models. :param int delta_t: The delta between two runs, in seconds. @@ -66,12 +66,16 @@ def __init__(self, shared_mtg, delta_t, :param bool option_static: Whether the model should be run for a static plant architecture :param None or dict update_parameters: A dictionary with the parameters to update, should have the form {'param1': value1, 'param2': value2, ...}. :param bool update_shared_df: If `True` update the shared dataframes at init and at each run (unless stated otherwise) + :param bool explicit_tillers: if True, compute real elongation kinetics for every tiller axis instead of + mirroring the main stem's geometry onto them. """ self._shared_mtg = shared_mtg #: the MTG shared between all models + self.explicit_tillers = explicit_tillers + self._simulation = simulation.Simulation(delta_t=delta_t, hydraulics=hydraulics, optimal_growth_option=optimal_growth_option, - update_parameters=update_parameters) #: the simulator to use to run the model + update_parameters=update_parameters, explicit_tillers=explicit_tillers) #: the simulator to use to run the model self.geometrical_model = geometrical_model #: the model which deals with geometry self._phytoT = phytoT #: dataframe generated by Adel and needed by the method add_metamer() diff --git a/src/openalea/cnwgrass/integration/runner.py b/src/openalea/cnwgrass/integration/runner.py index d1c0f3c..7434cdc 100644 --- a/src/openalea/cnwgrass/integration/runner.py +++ b/src/openalea/cnwgrass/integration/runner.py @@ -88,7 +88,7 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin run_from_outputs=False, stored_times=None, show_3Dplant=False, hydraulics=False, stomatal_model_name='BWB', drought_trigger=None, rehydration_scenario=None, optimal_growth_option=False, option_static=False, - external_soil_model=False, tillers_replications=None, heterogeneous_canopy=True, + external_soil_model=False, tillers_replications=None, explicit_tillers=False, heterogeneous_canopy=True, update_parameters_all_models=None, step_callback=None, INPUTS_DIRPATH='inputs', METEO_FILENAME='meteo.csv', MANAGEMENT_FILENAME='management.csv', OUTPUTS_DIRPATH='outputs', POSTPROCESSING_DIRPATH='postprocessing', GRAPHS_DIRPATH='graphs'): @@ -114,7 +114,9 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin :param bool optimal_growth_option: if True the model will assume optimal growth conditions :param bool option_static: Whether the model should be run for a static plant architecture :param bool external_soil_model: whether an external soil model is coupled to cnmetabolism. If True, cnmetabolism will skip calculations made in soil and uptake N by roots - :param dict [str, float] tillers_replications: a dictionary with tiller id as key, and weight of replication as value. + :param dict [str, float] tillers_replications: a dictionary with tiller id as key, and weight of replication as value. Ignored by CN-Metabolism when `explicit_tillers` is True. + :param bool explicit_tillers: if True, CN-Metabolism builds and integrates a real Axis for every tiller found in the MTG (sharing the main stem's roots/phloem/grains/endosperm) instead of + lumping tillers into the main stem via `tillers_replications` weights. :param bool heterogeneous_canopy: Whether to create a duplicated heterogeneous canopy from the initial mtg. :param dict update_parameters_all_models: a dict to update model parameters {'cnmetabolism': {'organ1': {'param1': 'val1', 'param2': 'val2'}, @@ -317,7 +319,8 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin optimal_growth_option=optimal_growth_option, option_static=option_static, update_parameters=update_parameters_morphogenesis, - update_shared_df=UPDATE_SHARED_DF) + update_shared_df=UPDATE_SHARED_DF, + explicit_tillers=explicit_tillers) # -- CARIBU -- caribu_facade_ = caribu_facade.CaribuFacade(g, @@ -354,6 +357,7 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin shared_organs_inputs_outputs_df, shared_axes_inputs_outputs_df, shared_elements_inputs_outputs_df, + explicit_tillers=explicit_tillers, update_parameters=update_parameters_senescence, update_shared_df=UPDATE_SHARED_DF) @@ -375,6 +379,7 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin shared_elements_inputs_outputs_df, stomatal_model_name=stomatal_model_name, hydraulics=hydraulics, + explicit_tillers=explicit_tillers, update_parameters=update_parameters_gasexchange, update_shared_df=UPDATE_SHARED_DF) @@ -403,6 +408,7 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin shared_elements_inputs_outputs_df, shared_axes_inputs_outputs_df, hydraulics=hydraulics, + explicit_tillers=explicit_tillers, update_parameters=update_parameters_growth, update_shared_df=UPDATE_SHARED_DF) @@ -449,6 +455,7 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin shared_elements_inputs_outputs_df, shared_soils_inputs_outputs_df, tillers_replications=tillers_replications, + explicit_tillers=explicit_tillers, external_soil_model=external_soil_model, update_shared_df=UPDATE_SHARED_DF) @@ -500,7 +507,8 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin shared_elements_inputs_outputs_df, shared_organs_inputs_outputs_df, shared_soils_inputs_outputs_df, - update_shared_df=UPDATE_SHARED_DF) + update_shared_df=UPDATE_SHARED_DF, + explicit_tillers=explicit_tillers) # Run cnmetabolism with constant nitrates concentration in the soil if specified diff --git a/src/openalea/cnwgrass/integration/senescence_facade.py b/src/openalea/cnwgrass/integration/senescence_facade.py index fb177f9..ac2356b 100644 --- a/src/openalea/cnwgrass/integration/senescence_facade.py +++ b/src/openalea/cnwgrass/integration/senescence_facade.py @@ -44,6 +44,7 @@ def __init__(self, shared_mtg, delta_t, shared_organs_inputs_outputs_df, shared_axes_inputs_outputs_df, shared_elements_inputs_outputs_df, + explicit_tillers=False, update_parameters=None, update_shared_df=True, cnwgrass_roots=True): @@ -57,13 +58,15 @@ def __init__(self, shared_mtg, delta_t, :param pandas.DataFrame shared_organs_inputs_outputs_df: the dataframe of inputs and outputs at organs scale shared between all models. :param pandas.DataFrame shared_axes_inputs_outputs_df: the dataframe of inputs and outputs at axis scale shared between all models. :param pandas.DataFrame shared_elements_inputs_outputs_df: the dataframe of inputs and outputs at element scale shared between all models. + :param bool explicit_tillers: if True, compute real senescence (green area / mstruct loss) for every tiller's elements found in the MTG, instead of only the main stem's. :param dict or None update_parameters: A dictionary with the parameters to update, should have the form {'param1': value1, 'param2': value2, ...}. :param bool update_shared_df: If `True` update the shared dataframes at init and at each run (unless stated otherwise) """ self._shared_mtg = shared_mtg #: the MTG shared between all models self.cnwgrass_roots = cnwgrass_roots - self._simulation = simulation.Simulation(delta_t=delta_t, update_parameters=update_parameters, cnwgrass_roots=cnwgrass_roots) #: the simulator to use to run the model + self.explicit_tillers = explicit_tillers + self._simulation = simulation.Simulation(delta_t=delta_t, update_parameters=update_parameters, cnwgrass_roots=cnwgrass_roots, explicit_tillers=explicit_tillers) #: the simulator to use to run the model all_senescence_inputs_dict = converter.from_dataframes(model_roots_inputs_df, model_axes_inputs_df, model_elements_inputs_df) self._update_shared_MTG(all_senescence_inputs_dict['roots'], all_senescence_inputs_dict['axes'], all_senescence_inputs_dict['elements']) @@ -105,7 +108,7 @@ def _initialize_model(self): mtg_plant_index = int(self._shared_mtg.index(mtg_plant_vid)) for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): mtg_axis_label = self._shared_mtg.label(mtg_axis_vid) - if mtg_axis_label != 'MS': + if mtg_axis_label != 'MS' and not self.explicit_tillers: continue axis_id = (mtg_plant_index, mtg_axis_label) mtg_axis_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid) @@ -177,7 +180,7 @@ def _update_shared_MTG(self, senescence_roots_data_dict, senescence_axes_data_di mtg_plant_index = int(self._shared_mtg.index(mtg_plant_vid)) for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): mtg_axis_label = self._shared_mtg.label(mtg_axis_vid) - if mtg_axis_label != 'MS': + if mtg_axis_label != 'MS' and not self.explicit_tillers: continue # update the axis property in the MTG @@ -190,14 +193,15 @@ def _update_shared_MTG(self, senescence_roots_data_dict, senescence_axes_data_di for axis_data_name, axis_data_value in senescence_axis_data_dict.items(): self._shared_mtg.property(axis_data_name)[mtg_axis_vid] = axis_data_value - # update the roots in the MTG - if axis_id not in senescence_roots_data_dict: - continue - if 'roots' not in self._shared_mtg.get_vertex_property(mtg_axis_vid): - self._shared_mtg.property('roots')[mtg_axis_vid] = {} - mtg_roots_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid)['roots'] - for roots_data_name, roots_data_value in senescence_roots_data_dict.items(): - self._shared_mtg.property(roots_data_name)[mtg_axis_vid] = roots_data_value + # update the roots in the MTG (only ever produced for the main stem -- a tiller axis has no + # roots of its own, see cnmetabolism_facade -- so this must not skip the metamer/organ/element + # update loop below for a tiller just because it has no roots entry) + if axis_id in senescence_roots_data_dict: + if 'roots' not in self._shared_mtg.get_vertex_property(mtg_axis_vid): + self._shared_mtg.property('roots')[mtg_axis_vid] = {} + mtg_roots_properties = self._shared_mtg.get_vertex_property(mtg_axis_vid)['roots'] + for roots_data_name, roots_data_value in senescence_roots_data_dict.items(): + self._shared_mtg.property(roots_data_name)[mtg_axis_vid] = roots_data_value for mtg_metamer_vid in self._shared_mtg.components_iter(mtg_axis_vid): mtg_metamer_index = int(self._shared_mtg.index(mtg_metamer_vid)) diff --git a/src/openalea/cnwgrass/morphogenesis/model.py b/src/openalea/cnwgrass/morphogenesis/model.py index 960dc8e..3024696 100644 --- a/src/openalea/cnwgrass/morphogenesis/model.py +++ b/src/openalea/cnwgrass/morphogenesis/model.py @@ -130,7 +130,7 @@ def calculate_cumulated_thermal_time(self, sum_TT, temperature, delta_teq): :rtype: float """ if temperature > 0: - return sum_TT + delta_teq * self.parameters.Temp_Tref / 24.0 / 3600 # Conversion to days at 12°C + return sum_TT + delta_teq * self.parameters.Temp_Tref / 24.0 / 3600 # Conversion to days at 12�C else: return sum_TT @@ -233,8 +233,10 @@ def calculate_leaf_pseudostem_length(ligule_heights, bottom_hiddenzone_height, p :return: Distance for the leaf to emerge out of the pseudostem (m) :rtype: float """ - top_ligule_height = float( - max(ligule_heights[ligule_heights['phytomer'] < phytomer_id]['ligule height'])) # highest previous ligule + previous_ligule_heights = ligule_heights[ligule_heights['phytomer'] < phytomer_id]['ligule height'] + # No previous ligule on this axis (e.g. a tiller's basal phytomer, which has no coleoptile to anchor + # against unlike the main stem's phytomer 1) -- nothing above yet constrains the leaf. + top_ligule_height = float(max(previous_ligule_heights)) if len(previous_ligule_heights) != 0 else 0.0 leaf_pseudostem_length = top_ligule_height - bottom_hiddenzone_height return max(leaf_pseudostem_length, 0) @@ -243,9 +245,9 @@ def calculate_deltaL_preE(self, sucrose, leaf_L, amino_acids, mstruct, delta_teq xylem_water_potential=None): """ Delta of leaf length over delta_t as a function of sucrose and amino acids, from initiation to the emergence of the previous leaf. - :param float sucrose: Amount of sucrose (µmol C) + :param float sucrose: Amount of sucrose (�mol C) :param float leaf_L: Total leaf length (m) - :param float amino_acids: Amount of amino acids (µmol N) + :param float amino_acids: Amount of amino acids (�mol N) :param float mstruct: Structural mass (g) :param float delta_teq: Temperature-compensated time = time duration at a reference temperature (s) :param int leaf_rank: leaf phytomer number @@ -317,8 +319,8 @@ def calculate_deltaL_postE(self, prev_leaf_pseudo_age, leaf_pseudo_age, prev_lea :param float leaf_pseudo_age: Pseudo age of the leaf since beginning of automate elongation (s) :param float prev_leaf_L: Leaf length at previous time step (m) :param float leaf_Lmax: Final leaf length (m) - :param float sucrose: Amount of sucrose (µmol C) - :param float amino_acids: Amount of amino acids (µmol N) + :param float sucrose: Amount of sucrose (�mol C) + :param float amino_acids: Amount of amino acids (�mol N) :param float mstruct: Structural mass (g) :param int leaf_rank: rank of the leaf :param bool optimal_growth_option: if True the function will calculate leaf elongation assuming optimal growth conditions (except if sucrose and amino acids are zero) @@ -479,13 +481,13 @@ def calculate_mean_conc_sucrose(prev_mean_conc_sucrose, time_prev_leaf2_emergenc """ Update the mean sucrose concentration of the hiddenzone since leaf n-2 emergence. Calculation starts at leaf n-2 emergence, the updated mean accounts for the [sucrose] of current time step weighted by a function of temperature. - :param float prev_mean_conc_sucrose: Mean sucrose concentration of the hiddenzone at the end of previous simulation time step (µmol C g-1). + :param float prev_mean_conc_sucrose: Mean sucrose concentration of the hiddenzone at the end of previous simulation time step (�mol C g-1). :param float time_prev_leaf2_emergence: Time elapsed since leaf n-2 emergence (s at Tref). :param float delta_teq: Duration of the current simulation time step (s at Tref). - :param float sucrose: Sucrose in the hidden zone (µmol C). + :param float sucrose: Sucrose in the hidden zone (�mol C). :param float mstruct: Mstruct of the hidden zone (g). - :return: Updated mean sucrose concentration of the hiddenzone since leaf n-2 emergence (µmol C g-1) + :return: Updated mean sucrose concentration of the hiddenzone since leaf n-2 emergence (�mol C g-1) :rtype: float """ conc_sucrose = sucrose / mstruct @@ -504,7 +506,7 @@ def calculate_leaf_Wmax(self, lamina_Lmax, leaf_rank, integral_conc_sucr, optima :param float lamina_Lmax: Maximal lamina length (m) :param int leaf_rank: leaf phytomer number - :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (µmol C g-1) + :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (�mol C g-1) :param bool optimal_growth_option: if True the function will calculate leaf Wmax assuming optimal growth conditions :return: Maximal leaf width (m) @@ -527,7 +529,7 @@ def calculate_SSLW(self, leaf_rank, integral_conc_sucr, optimal_growth_option=Fa """ Structural Specific Lamina Weight. :param int leaf_rank: leaf phytomer number - :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (µmol C g-1) + :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (�mol C g-1) :param bool optimal_growth_option: if True the function will calculate SSLW assuming optimal growth conditions @@ -548,7 +550,7 @@ def calculate_LSSW(self, leaf_rank, integral_conc_sucr, optimal_growth_option=Fa """ Lineic Structural Sheath Weight. :param int leaf_rank: leaf phytomer number - :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (µmol C g-1) + :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (�mol C g-1) :param bool optimal_growth_option: if True the function will calculate LSLW assuming optimal growth conditions :return: Lineic Structural Sheath Weight (g m-1) @@ -618,8 +620,10 @@ def calculate_internode_distance_to_emerge(ligule_heights, bottom_hiddenzone_hei :rtype: float """ - top_ligule_height = float( - max(ligule_heights[ligule_heights['phytomer'] < phytomer_rank]['ligule height'])) # highest previous ligule + previous_ligule_heights = ligule_heights[ligule_heights['phytomer'] < phytomer_rank]['ligule height'] + # No previous ligule on this axis (e.g. a tiller's basal phytomer, which has no coleoptile to anchor + # against unlike the main stem's phytomer 1) -- nothing above yet constrains the leaf. + top_ligule_height = float(max(previous_ligule_heights)) if len(previous_ligule_heights) != 0 else 0.0 leaf_pseudostem_length = top_ligule_height - bottom_hiddenzone_height internode_distance_to_emerge = leaf_pseudostem_length + curr_internode_L @@ -688,9 +692,9 @@ def calculate_delta_internode_L_preL(self, phytomer_rank, sucrose, internode_L, """ delta of internode length over delta_t as a function of sucrose and amino acids, from initiation to the ligulation of the previous leaf. :param int phytomer_rank: phytomer rank - :param float sucrose: Amount of sucrose (µmol C) + :param float sucrose: Amount of sucrose (�mol C) :param float internode_L: Total internode length (m) - :param float amino_acids: Amount of amino acids (µmol N) + :param float amino_acids: Amount of amino acids (�mol N) :param float mstruct: Structural mass of the hidden zone(g) :param float delta_teq: Temperature-compensated time = time duration at a reference temperature (s) :param bool optimal_growth_option: if True the function will calculate delta of internode length assuming optimal growth conditions @@ -772,9 +776,9 @@ def calculate_delta_internode_L_postL(self, prev_internode_pseudo_age, internode :param float internode_pseudo_age: Pseudo age of the internode since beginning of automate elongation (s) :param float prev_internode_L: Internode length before elongation (m) :param float internode_Lmax_lig: Estimate of final internode length at previous leaf ligulation (m) - :param float sucrose: Amount of sucrose (µmol C) - :param float amino_acids: Amount of amino acids (µmol N) - :param float mstruct: Structural mass (µmol N) + :param float sucrose: Amount of sucrose (�mol C) + :param float amino_acids: Amount of amino acids (�mol N) + :param float mstruct: Structural mass (�mol N) :param bool optimal_growth_option: if True the function will calculate delta of internode length assuming optimal growth conditions :return: internode_L (m) @@ -932,9 +936,9 @@ def calculate_deltaL_preE(self, sucrose, leaf_L, amino_acids, mstruct, delta_teq xylem_water_potential=0.): """ Delta of leaf length over delta_t as a function of sucrose and amino acids, from initiation to the emergence of the previous leaf. - :param float sucrose: Amount of sucrose (µmol C) + :param float sucrose: Amount of sucrose (�mol C) :param float leaf_L: Total leaf length (m) - :param float amino_acids: Amount of amino acids (µmol N) + :param float amino_acids: Amount of amino acids (�mol N) :param float mstruct: Structural mass (g) :param float delta_teq: Temperature-compensated time = time duration at a reference temperature (s) :param int leaf_rank: leaf phytomer number diff --git a/src/openalea/cnwgrass/morphogenesis/simulation.py b/src/openalea/cnwgrass/morphogenesis/simulation.py index ead0529..2e481c5 100644 --- a/src/openalea/cnwgrass/morphogenesis/simulation.py +++ b/src/openalea/cnwgrass/morphogenesis/simulation.py @@ -31,12 +31,14 @@ class Simulation(object): """The Simulation class allows to initialize and run a simulation. """ - def __init__(self, delta_t=1, hydraulics=False, optimal_growth_option=False, update_parameters=None): + def __init__(self, delta_t=1, hydraulics=False, optimal_growth_option=False, update_parameters=None, explicit_tillers=False): """ :param int delta_t: the delta t of the simulation (in seconds) :param bool hydraulics: if True the model will assume the coupling to the turgor-driven growth model :param bool optimal_growth_option: if True the model will assume optimal growth conditions :param None or dict update_parameters: if a dict is provided, the specified parameters in keys will be updated + :param bool explicit_tillers: if True, compute real elongation kinetics for every tiller axis's hiddenzones + instead of mirroring the main stem's -- see the `axe_label != 'MS'` guard in :meth:`run`. """ #: `inputs` is a dictionary of dictionaries: @@ -79,6 +81,10 @@ def __init__(self, delta_t=1, hydraulics=False, optimal_growth_option=False, upd #: Optimal growth option self.optimal_growth_option = optimal_growth_option + #: if True, compute real elongation kinetics for every tiller axis's hiddenzones (instead of mirroring + #: the main stem's geometry onto them every timestep) -- see :meth:`run`. + self.explicit_tillers = explicit_tillers + #: Update parameters if specified if update_parameters: for key, value in update_parameters.items(): @@ -204,8 +210,8 @@ def run(self, Tair, Tsoil, Zsowing=0.025): xylem_water_potential = all_axes_inputs[axis_id].get('xylem_water_potential') # Set at None if hydraulics is False hz_turgor_water_potential = hiddenzone_inputs.get('turgor_water_potential') # Set at None if hydraulics is False - #: Tillers: in this version tillers functioning is replicated from corresponding elements of MS - if axe_label != 'MS': + #: Tillers: unless explicit_tillers is set, tiller functioning is replicated from corresponding elements of MS + if axe_label != 'MS' and not self.explicit_tillers: tiller_to_MS_phytomer_id = tuple([axis_id[0], 'MS', all_axes_outputs[axis_id]['cohort'] + phytomer_id - 1]) if tiller_to_MS_phytomer_id in all_hiddenzone_outputs.keys(): @@ -289,14 +295,27 @@ def run(self, Tair, Tsoil, Zsowing=0.025): below_internode_lengths = all_sheath_internode_lengths[axis_id][phytomer_id]['cumulated_internode'] bottom_hiddenzone_height = self.model.calculate_cumulated_internode_length(below_internode_lengths) + # Ligule heights used to anchor pseudostem/internode-emergence distances: normally the axis's own + # previously-ligulated leaves. A tiller's basal phytomer has no coleoptile and thus no earlier + # entry on its own axis -- it grows enclosed within the sheath of the subtending main-stem leaf + # (at MS phytomer = cohort) instead, so anchor to the main stem's ligule heights up to that + # phytomer in that case. + own_axis_ligule_heights = all_ligule_height_df[all_ligule_height_df['axis_id'] == axis_id] + if axe_label != 'MS' and not (own_axis_ligule_heights['phytomer'] < phytomer_id).any(): + pseudostem_ligule_heights = all_ligule_height_df[all_ligule_height_df['axis_id'] == (axis_id[0], 'MS')] + pseudostem_phytomer_id = curr_axis_outputs['cohort'] + 1 # select up to (and including) the subtending MS leaf + else: + pseudostem_ligule_heights = own_axis_ligule_heights + pseudostem_phytomer_id = phytomer_id + # Distance between the bottom of the hiddenzone and the highest previous ligule - leaf_pseudostem_length = self.model.calculate_leaf_pseudostem_length(all_ligule_height_df[all_ligule_height_df['axis_id'] == axis_id], bottom_hiddenzone_height, phytomer_id) + leaf_pseudostem_length = self.model.calculate_leaf_pseudostem_length(pseudostem_ligule_heights, bottom_hiddenzone_height, pseudostem_phytomer_id) curr_hiddenzone_outputs['leaf_pseudostem_length'] = leaf_pseudostem_length curr_hiddenzone_outputs['delta_leaf_pseudostem_length'] = leaf_pseudostem_length - hiddenzone_inputs['leaf_pseudostem_length'] # Variable used in growth # Calculate the internode pseudostem length curr_internode_L = hiddenzone_inputs['internode_L'] - internode_distance_to_emerge = self.model.calculate_internode_distance_to_emerge(all_ligule_height_df[all_ligule_height_df['axis_id'] == axis_id], bottom_hiddenzone_height, phytomer_id, + internode_distance_to_emerge = self.model.calculate_internode_distance_to_emerge(pseudostem_ligule_heights, bottom_hiddenzone_height, pseudostem_phytomer_id, curr_internode_L) curr_hiddenzone_outputs['internode_distance_to_emerge'] = internode_distance_to_emerge curr_hiddenzone_outputs['delta_internode_distance_to_emerge'] = internode_distance_to_emerge - hiddenzone_inputs['internode_distance_to_emerge'] # Variable used in growth diff --git a/src/openalea/cnwgrass/senescence/simulation.py b/src/openalea/cnwgrass/senescence/simulation.py index fd305b7..c62b223 100644 --- a/src/openalea/cnwgrass/senescence/simulation.py +++ b/src/openalea/cnwgrass/senescence/simulation.py @@ -18,7 +18,7 @@ class Simulation(object): """The Simulation class permits to initialize and run a simulation. """ - def __init__(self, delta_t=1, update_parameters=None, cnwgrass_roots=True): + def __init__(self, delta_t=1, update_parameters=None, cnwgrass_roots=True, explicit_tillers=False): #: The inputs of Senescence. #: @@ -42,6 +42,13 @@ def __init__(self, delta_t=1, update_parameters=None, cnwgrass_roots=True): parameters.__dict__.update(update_parameters) self.cnwgrass_roots = cnwgrass_roots + + #: if True, compute senescence for every tiller axis's elements found in :attr:`inputs`, not just the + #: main stem's -- see the `axe_label != 'MS'` guard in :meth:`run`. Must stay in sync with + #: SENESCENCEFacade's own explicit_tillers flag, which controls whether tiller elements are collected + #: into `inputs` in the first place. + self.explicit_tillers = explicit_tillers + def initialize(self, inputs): """ Initialize :attr:`inputs` from `inputs`. @@ -98,7 +105,7 @@ def run(self, forced_max_protein_elements=None, opt_full_remob=False, postflower for element_inputs_id, element_inputs_dict in all_elements_inputs.items(): axe_label = element_inputs_id[1] - if axe_label != 'MS': # TODO: Calculation only for the main stem + if axe_label != 'MS' and not self.explicit_tillers: continue # Temperature-compensated time (delta_teq) From 8d32ed72dba93d57626350fb57e3a549302150c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 3 Sep 2026 11:50:45 +0200 Subject: [PATCH 2/3] fixed ratio in hiddenzones initialization --- .../cnwgrass/integration/hydraulics_facade.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/openalea/cnwgrass/integration/hydraulics_facade.py b/src/openalea/cnwgrass/integration/hydraulics_facade.py index 51c2af1..9f70682 100644 --- a/src/openalea/cnwgrass/integration/hydraulics_facade.py +++ b/src/openalea/cnwgrass/integration/hydraulics_facade.py @@ -307,6 +307,25 @@ def _build_axis_phytomers(self, mtg_axis_vid, hydraulics_axis): hydraulics_hiddenzone.initialize() + #: width/thickness fall back to the class's flat "just initiated" defaults the same way as + #: leaf_Wmax/lamina_Lmax below when exempted above -- but unlike those two (which mirror + #: leaf_L/width themselves and so stay self-consistent), that flat default is wrong whenever + #: leaf_L is already non-trivial (a tiller emerging into hydraulics tracking mid-growth, e.g. + #: under explicit_tillers): it produces a volume wildly inconsistent with the hiddenzone's + #: real size, which then feeds a badly distorted osmotic/turgor potential and drives a + #: spurious multi-hundred-hour elongation transient as the model "corrects" this + #: self-inflicted geometry. Derive them proportionally from the hiddenzone's own leaf_L + #: instead (always genuinely tracked by morphogenesis regardless of hydraulics), using the + #: same WL_ratio/TL_ratio the model itself applies at true leaf_pseudo_age == 0 initiation + #: (see hydraulics/simulation.py) -- correct and self-consistent whether this hiddenzone is + #: genuinely fresh (tiny leaf_L) or already partway grown. + if 'width' in missing_initial_hiddenzone_properties and not self._is_unusable_mtg_value(hydraulics_hiddenzone.leaf_L): + print("width not default anymore") + hydraulics_hiddenzone.width = hydraulics_hiddenzone.leaf_L * hydraulics_model.HiddenZone.PARAMETERS.WL_ratio + if 'thickness' in missing_initial_hiddenzone_properties and not self._is_unusable_mtg_value(hydraulics_hiddenzone.leaf_L): + print("thickness not default anymore") + hydraulics_hiddenzone.thickness = hydraulics_hiddenzone.leaf_L * hydraulics_model.HiddenZone.PARAMETERS.TL_ratio + #: leaf_Wmax/lamina_Lmax default to None (not a usable finite value like water_content/width/ #: thickness/turgor_water_potential do) -- if either is still unusable at this point (missing #: from the MTG or exempted above), seed it the same way the model itself does the first time From ba36f13324872b04848e48b2a8bcc154a043a816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Sat, 5 Sep 2026 11:21:09 +0200 Subject: [PATCH 3/3] current debugging of current tillers separation --- src/openalea/cnwgrass/cnmetabolism/model.py | 692 +++++++++--------- .../cnwgrass/cnmetabolism/simulation.py | 10 +- .../cnwgrass/gasexchange/simulation.py | 6 +- src/openalea/cnwgrass/growth/simulation.py | 6 +- src/openalea/cnwgrass/hydraulics/model.py | 36 +- .../integration/cnmetabolism_facade.py | 8 +- .../integration/gasexchange_facade.py | 2 +- .../cnwgrass/integration/hydraulics_facade.py | 26 +- .../cnwgrass/integration/senescence_facade.py | 2 +- src/openalea/cnwgrass/morphogenesis/model.py | 36 +- .../cnwgrass/morphogenesis/parameters.py | 9 +- .../cnwgrass/morphogenesis/simulation.py | 20 +- 12 files changed, 451 insertions(+), 402 deletions(-) diff --git a/src/openalea/cnwgrass/cnmetabolism/model.py b/src/openalea/cnwgrass/cnmetabolism/model.py index 2d1ed62..44c64ee 100644 --- a/src/openalea/cnwgrass/cnmetabolism/model.py +++ b/src/openalea/cnwgrass/cnmetabolism/model.py @@ -114,9 +114,9 @@ def calculate_aggregated_variables(self): @staticmethod def calculate_temperature_effect_on_conductivity(Tair): """Effect of the temperature on phloem translocation conductivity (Farrar 1988) - Should multiply the rate at 20�C + Should multiply the rate at 20°C - :param float Tair: Air temperature (�C) + :param float Tair: Air temperature (°C) :return: Correction to apply to conductivity coefficients. :rtype: float @@ -129,9 +129,9 @@ def calculate_temperature_effect_on_conductivity(Tair): @staticmethod def calculate_temperature_effect_on_Vmax(Tair): """Effect of the temperature on maximal enzyme activity - Should multiply the rate at 20�C + Should multiply the rate at 20°C - :param float Tair: Air temperature (�C) + :param float Tair: Air temperature (°C) :return: Correction to apply to enzyme activity :rtype: float @@ -193,7 +193,7 @@ def __init__(self, label=None, roots=None, phloem=None, grains=None, endosperm=N self.Total_Transpiration = None #: the total transpiration (mmol s-1) self.mstruct = None #: structural mass of the axis (g) self.senesced_mstruct = None #: senesced structural mass of the axis (g) - self.nitrates = None #: nitrates in the axis (�mol N) + self.nitrates = None #: nitrates in the axis (µmol N) def calculate_aggregated_variables(self): """Calculate the integrative variables of the axis recursively. @@ -251,10 +251,10 @@ def __init__(self, index=None, chaff=None, peduncle=None, lamina=None, internode self.hiddenzone = hiddenzone #: the hidden zone self.mstruct = None #: the structural mass of the phytomer (g) self.senesced_mstruct = None #: senesced structural mass of the phytomer (g) - self.nitrates = None #: nitrates of the phytomer (�mol N) + self.nitrates = None #: nitrates of the phytomer (µmol N) if cohorts is None: cohorts = [] - # TODO: Hack to deal with tillering cases: TEMPORARY.Devrait �tre port� � l'�chelle de la plante uniquement mais je ne vois pas comment faire mieux + # TODO: Hack to deal with tillering cases: TEMPORARY.Devrait être porté à l'échelle de la plante uniquement mais je ne vois pas comment faire mieux self.cohorts = cohorts #: list of cohort values self.cohorts_replications = cohorts_replications #: dictionary of number of replications per cohort rank @@ -311,17 +311,17 @@ def __init__(self, label='endosperm', starch=0, proteins=0, mstruct=0, moistenin super(Endosperm, self).__init__(label) # state variables - self.starch = starch #: �mol` of C starch (endosperm) - self.proteins = proteins #: �mol` of N (endosperm) + self.starch = starch #: µmol` of C starch (endosperm) + self.proteins = proteins #: µmol` of N (endosperm) self.mstruct = mstruct #: g of MS (~ pericarp) self.moistening = moistening # Progression of seed moistening (relative, 0 to 1) # fluxes to the phloem - self.D_starch = None #: current degradation of starch integrated over a delta t (�mol` C g-1) - self.D_proteins = None #: current degradation of proteins integrated over a delta t (�mol` N g-1) + self.D_starch = None #: current degradation of starch integrated over a delta t (µmol` C g-1) + self.D_proteins = None #: current degradation of proteins integrated over a delta t (µmol` N g-1) # intermediate variables - self.R_residual = None #: maintenance respiration of endosperm (�mol` C respired) + self.R_residual = None #: maintenance respiration of endosperm (µmol` C respired) # VARIABLES @staticmethod @@ -338,7 +338,7 @@ def modified_Arrhenius_equation(temperature): # TODO: move in a seperate model Temp_Ea_R = 8900 # Parameter Ea/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (K) Temp_DS_R = 68.432 # Parameter deltaS/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (dimensionless) Temp_DH_R = 20735.5 # Parameter deltaH/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (K) - Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(�C) + Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(°C) def Arrhenius_equation(T): return T * exp(-Temp_Ea_R / T) / (1 + exp(Temp_DS_R - Temp_DH_R / T)) @@ -358,9 +358,9 @@ def calculate_temperature_effect_on_growth(self, Tair): """Effect of the temperature on seed. Return value of equation from Johnson and Lewin (1946) for temperature. The equation is modified to return zero below zero degree. Identical to modified_Arrhenius_equation in Morphogenesis. - Should multiply the rate at 20�C + Should multiply the rate at 20°C - :param float Tair: Air temperature(�C) + :param float Tair: Air temperature(°C) :return: Correction to apply to endosperm remobilisation (dimensionless) :rtype: float @@ -374,13 +374,13 @@ def calculate_moistening(): # FLUXES @staticmethod def calculate_D_starch(starch, T_effect_Vmax): - """Rate of starch degradation from seed endosperm (�mol` C starch h-1). + """Rate of starch degradation from seed endosperm (µmol` C starch h-1). First order kinetic. - :param float starch: Amount of starch (�mol` C) + :param float starch: Amount of starch (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Starch degradation (�mol` C h-1) + :return: Starch degradation (µmol` C h-1) :rtype: float """ return max(0., min(starch, Endosperm.PARAMETERS.K_STARCH * (Endosperm.PARAMETERS.STARCH_MAX - starch) * (starch - Endosperm.PARAMETERS.STARCH_MIN) * \ @@ -390,10 +390,10 @@ def calculate_D_starch(starch, T_effect_Vmax): def calculate_D_proteins(proteins, T_effect_Vmax): """Protein degradation in seed endosperm. - :param float proteins: Protein amount in endosperm (�mol` N) + :param float proteins: Protein amount in endosperm (µmol` N) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Proteins degradation (�mol` N h-1) + :return: Proteins degradation (µmol` N h-1) :rtype: float """ return max(0., min(proteins, Endosperm.PARAMETERS.K_PROTEINS * (Endosperm.PARAMETERS.PROTEINS_MAX - proteins) * (proteins - Endosperm.PARAMETERS.PROTEINS_MIN) * \ @@ -404,10 +404,10 @@ def calculate_D_proteins(proteins, T_effect_Vmax): def calculate_starch_derivative(D_grain_starch, R_residual): """delta starch of seed endosperm. - :param float D_grain_starch: Degradation of starch in endosperm (�mol` C g-1 h-1) - :param float R_residual: Residual respiration (maintenance) of the seed (�mol` C respired) + :param float D_grain_starch: Degradation of starch in endosperm (µmol` C g-1 h-1) + :param float R_residual: Residual respiration (maintenance) of the seed (µmol` C respired) - :return: delta seed starch (�mol` C starch) + :return: delta seed starch (µmol` C starch) :rtype: float """ return -D_grain_starch - R_residual @@ -416,9 +416,9 @@ def calculate_starch_derivative(D_grain_starch, R_residual): def calculate_proteins_derivative(D_Proteins): """delta proteins of seed endosperm. - :param float D_Proteins: Degradation of proteins in endosperm (�mol` N g-1) + :param float D_Proteins: Degradation of proteins in endosperm (µmol` N g-1) - :return: delta grain proteins (�mol` N proteins) + :return: delta grain proteins (µmol` N proteins) :rtype: float """ return -D_Proteins @@ -440,7 +440,7 @@ def __init__(self, label='hiddenzone', mstruct=INIT_COMPARTMENTS.mstruct, Nstruc if cohorts is None: cohorts = [] - # TODO: Hack to deal with tillering cases: TEMPORARY.Devrait �tre port� � l'�chelle de la plante uniquement mais je ne vois pas comment faire mieux + # TODO: Hack to deal with tillering cases: TEMPORARY.Devrait être porté à l'échelle de la plante uniquement mais je ne vois pas comment faire mieux self.cohorts = cohorts #: list of cohort values self.cohorts_replications = cohorts_replications #: dictionary of number of replications per cohort rank self.index = index #: the index of the phytomer TEMPORARY @@ -452,26 +452,26 @@ def __init__(self, label='hiddenzone', mstruct=INIT_COMPARTMENTS.mstruct, Nstruc self.is_over = is_over # state variables - self.sucrose = sucrose #: �mol` C - self.fructan = fructan #: �mol` C - self.amino_acids = amino_acids #: �mol` N - self.proteins = proteins #: �mol` N + self.sucrose = sucrose #: µmol` C + self.fructan = fructan #: µmol` C + self.amino_acids = amino_acids #: µmol` N + self.proteins = proteins #: µmol` N # fluxes from phloem - self.Unloading_Sucrose = None #: current Unloading of sucrose from phloem to hiddenzone integrated over delta t (�mol` C) - self.Unloading_Amino_Acids = None #: current Unloading of amino acids from phloem to hiddenzone integrated over delta t (�mol` N) + self.Unloading_Sucrose = None #: current Unloading of sucrose from phloem to hiddenzone integrated over delta t (µmol` C) + self.Unloading_Amino_Acids = None #: current Unloading of amino acids from phloem to hiddenzone integrated over delta t (µmol` N) # other fluxes - self.S_Proteins = None #: protein synthesis (�mol` N g-1 mstruct) - self.S_Fructan = None #: fructan synthesis (�mol` C g-1 mstruct) - self.D_Fructan = None #: fructan degradation (�mol` C g-1 mstruct) - self.D_Proteins = None #: protein degradation (�mol` N g-1 mstruct) + self.S_Proteins = None #: protein synthesis (µmol` N g-1 mstruct) + self.S_Fructan = None #: fructan synthesis (µmol` C g-1 mstruct) + self.D_Fructan = None #: fructan degradation (µmol` C g-1 mstruct) + self.D_Proteins = None #: protein degradation (µmol` N g-1 mstruct) # intermediate variables - self.R_residual = None #: Residual maintenance respiration (cost from protein turn-over, cell ion gradients, futile cycles...) (�mol` C respired) + self.R_residual = None #: Residual maintenance respiration (cost from protein turn-over, cell ion gradients, futile cycles...) (µmol` C respired) # Integrated variables - self.Total_Organic_Nitrogen = None #: current total nitrogen amount (�mol` N) + self.Total_Organic_Nitrogen = None #: current total nitrogen amount (µmol` N) @property def nb_replications(self): @@ -486,11 +486,11 @@ def calculate_Total_Organic_Nitrogen(amino_acids, proteins, Nstruct): """Total amount of organic N (amino acids + proteins + Nstruct). Used to calculate residual respiration. - :param float amino_acids: Amount of amino acids (�mol` N) - :param float proteins: Amount of proteins (�mol` N) + :param float amino_acids: Amount of amino acids (µmol` N) + :param float proteins: Amount of proteins (µmol` N) :param float Nstruct: Structural N mass (g) - :return: Total amount of organic N (�mol` N) + :return: Total amount of organic N (µmol` N) :rtype: float """ return amino_acids + proteins + (Nstruct / EcophysiologicalConstants.N_MOLAR_MASS) * 1E6 @@ -498,15 +498,15 @@ def calculate_Total_Organic_Nitrogen(amino_acids, proteins, Nstruct): # FLUXES def calculate_Unloading_Sucrose(self, sucrose, sucrose_phloem, mstruct_axis, T_effect_conductivity): - """Rate of sucrose Unloading from phloem to the hidden zone (�mol` C sucrose unloaded h-1). + """Rate of sucrose Unloading from phloem to the hidden zone (µmol` C sucrose unloaded h-1). Transport-resistance equation - :param float sucrose: Sucrose amount in the hidden zone (�mol` C) - :param float sucrose_phloem: Sucrose amount in phloem (�mol` C) + :param float sucrose: Sucrose amount in the hidden zone (µmol` C) + :param float sucrose_phloem: Sucrose amount in phloem (µmol` C) :param float mstruct_axis: The structural dry mass of the axis (g) - :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20�C (AU) + :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20°C (AU) - :return: Rate of Sucrose Unloading (�mol` C h-1) + :return: Rate of Sucrose Unloading (µmol` C h-1) :rtype: float """ conc_sucrose_phloem = (sucrose_phloem / mstruct_axis) @@ -516,15 +516,15 @@ def calculate_Unloading_Sucrose(self, sucrose, sucrose_phloem, mstruct_axis, T_e return (conc_sucrose_phloem - conc_sucrose_HZ) * conductance * parameters.SECOND_TO_HOUR_RATE_CONVERSION def calculate_Unloading_Amino_Acids(self, amino_acids, amino_acids_phloem, mstruct_axis, T_effect_conductivity): - """Rate of amino acids Unloading from phloem to the hidden zone (�mol` N amino acids unloaded h-1). + """Rate of amino acids Unloading from phloem to the hidden zone (µmol` N amino acids unloaded h-1). Transport-resistance equation - :param float amino_acids: Amino_acids amount in the hidden zone (�mol` N) - :param float amino_acids_phloem: Amino_acids amount in phloem (�mol` N) + :param float amino_acids: Amino_acids amount in the hidden zone (µmol` N) + :param float amino_acids_phloem: Amino_acids amount in phloem (µmol` N) :param float mstruct_axis: The structural dry mass of the axis (g) - :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20�C (AU) + :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20°C (AU) - :return: Rate of Amino_acids Unloading (�mol` N h-1) + :return: Rate of Amino_acids Unloading (µmol` N h-1) :rtype: float """ conc_amino_acids_phloem = (amino_acids_phloem / mstruct_axis) @@ -533,37 +533,37 @@ def calculate_Unloading_Amino_Acids(self, amino_acids, amino_acids_phloem, mstru return (conc_amino_acids_phloem - conc_amino_acids_HZ) * conductance * parameters.SECOND_TO_HOUR_RATE_CONVERSION def calculate_S_proteins(self, amino_acids, T_effect_Vmax): - """Rate of protein synthesis (�mol` N proteins h-1 g-1 MS). + """Rate of protein synthesis (µmol` N proteins h-1 g-1 MS). Michaelis-Menten function of amino acids. - :param float amino_acids: Amino acid amount in the hidden zone (�mol` N) + :param float amino_acids: Amino acid amount in the hidden zone (µmol` N) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Protein synthesis (�mol` N g-1 mstruct h-1) + :return: Rate of Protein synthesis (µmol` N g-1 mstruct h-1) :rtype: float """ vmax = HiddenZone.PARAMETERS.VMAX_SPROTEINS_EMZ * (1 - self.ratio_DZ) + HiddenZone.PARAMETERS.VMAX_SPROTEINS_DZ * self.ratio_DZ #: 'Mean' Vmax for the whole hidden zone return ((vmax * max(0, (amino_acids / self.mstruct))) / (HiddenZone.PARAMETERS.K_SPROTEINS + max(0, (amino_acids / self.mstruct)))) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax def calculate_D_Proteins(self, proteins, T_effect_Vmax): - """Rate of protein degradation (�mol` N proteins h-1 g-1 MS). + """Rate of protein degradation (µmol` N proteins h-1 g-1 MS). First order kinetic - :param float proteins: Protein amount in the hidden zone (�mol` N) + :param float proteins: Protein amount in the hidden zone (µmol` N) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Protein degradation (�mol` N g-1 mstruct h-1) + :return: Rate of Protein degradation (µmol` N g-1 mstruct h-1) :rtype: float """ return max(0, (HiddenZone.PARAMETERS.delta_Dproteins * (proteins / self.mstruct))) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax def calculate_Regul_S_Fructan(self, Unloading_Sucrose): """Regulating function for fructan maximal rate of synthesis. - Negative regulation by the loading of sucrose from the phloem ("switch-off" sigmo�dal kinetic). + Negative regulation by the loading of sucrose from the phloem ("switch-off" sigmoïdal kinetic). - :param float Unloading_Sucrose: Sucrose unloading (�mol` C) + :param float Unloading_Sucrose: Sucrose unloading (µmol` C) - :return: Maximal rate of fructan synthesis (�mol` C g-1 mstruct) + :return: Maximal rate of fructan synthesis (µmol` C g-1 mstruct) :rtype: float """ @@ -581,28 +581,28 @@ def calculate_Regul_S_Fructan(self, Unloading_Sucrose): return Vmax_Sfructans def calculate_S_Fructan(self, sucrose, Regul_S_Fructan, T_effect_Vmax): - """Rate of fructan synthesis (�mol` C fructan g-1 mstruct h-1). - Sigmo�dal function of sucrose. + """Rate of fructan synthesis (µmol` C fructan g-1 mstruct h-1). + Sigmoïdal function of sucrose. - :param float sucrose: Amount of sucrose (�mol` C) - :param float Regul_S_Fructan: Maximal rate of fructan synthesis regulated by sucrose loading (�mol` C g-1 mstruct) + :param float sucrose: Amount of sucrose (µmol` C) + :param float Regul_S_Fructan: Maximal rate of fructan synthesis regulated by sucrose loading (µmol` C g-1 mstruct) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Fructan synthesis (�mol` C g-1 mstruct) + :return: Rate of Fructan synthesis (µmol` C g-1 mstruct) :rtype: float """ return ((max(0., sucrose) / self.mstruct) * HiddenZone.PARAMETERS.VMAX_SFRUCTAN_RELATIVE * Regul_S_Fructan) / \ ((max(0., sucrose) / self.mstruct) + HiddenZone.PARAMETERS.K_SFRUCTAN) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax def calculate_D_Fructan(self, sucrose, fructan, T_effect_Vmax): - """Rate of fructan degradation (�mol` C fructan g-1 mstruct h-1). + """Rate of fructan degradation (µmol` C fructan g-1 mstruct h-1). Inhibition function by the end product i.e. sucrose (Bancal et al., 2012). - :param float sucrose: Amount of sucrose (�mol` C) - :param float fructan: Amount of fructan (�mol` C) + :param float sucrose: Amount of sucrose (µmol` C) + :param float fructan: Amount of fructan (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Fructan degradation (�mol` C g-1 mstruct) + :return: Rate of Fructan degradation (µmol` C g-1 mstruct) :rtype: float """ d_potential = ((HiddenZone.PARAMETERS.K_DFRUCTAN * HiddenZone.PARAMETERS.VMAX_DFRUCTAN * T_effect_Vmax) / @@ -615,13 +615,13 @@ def calculate_D_Fructan(self, sucrose, fructan, T_effect_Vmax): def calculate_sucrose_derivative(self, Unloading_Sucrose, S_Fructan, D_Fructan, hiddenzone_Loading_Sucrose_contribution, R_residual): """delta sucrose of hidden zone. - :param float Unloading_Sucrose: Sucrose unloaded (�mol` C) - :param float S_Fructan: Fructan synthesis (�mol` C g-1 mstruct) - :param float D_Fructan: Fructan degradation (�mol` C g-1 mstruct) - :param float hiddenzone_Loading_Sucrose_contribution: Sucrose imported from the emerged tissues (�mol` C) - :param float R_residual: Residual respiration (�mol` C) + :param float Unloading_Sucrose: Sucrose unloaded (µmol` C) + :param float S_Fructan: Fructan synthesis (µmol` C g-1 mstruct) + :param float D_Fructan: Fructan degradation (µmol` C g-1 mstruct) + :param float hiddenzone_Loading_Sucrose_contribution: Sucrose imported from the emerged tissues (µmol` C) + :param float R_residual: Residual respiration (µmol` C) - :return: delta sucrose (�mol` C sucrose) + :return: delta sucrose (µmol` C sucrose) :rtype: float """ return Unloading_Sucrose + (D_Fructan - S_Fructan) * self.mstruct + hiddenzone_Loading_Sucrose_contribution - R_residual @@ -629,12 +629,12 @@ def calculate_sucrose_derivative(self, Unloading_Sucrose, S_Fructan, D_Fructan, def calculate_amino_acids_derivative(self, Unloading_Amino_Acids, S_Proteins, D_Proteins, hiddenzone_Loading_Amino_Acids_contribution): """delta amino acids of hidden zone. - :param float Unloading_Amino_Acids: Amino acids unloaded (�mol` N) - :param float S_Proteins: Protein synthesis (�mol` N g-1 mstruct) - :param float D_Proteins: Protein degradation (�mol` N g-1 mstruct) - :param float hiddenzone_Loading_Amino_Acids_contribution: Amino acids imported from the emerged tissues (�mol` N) + :param float Unloading_Amino_Acids: Amino acids unloaded (µmol` N) + :param float S_Proteins: Protein synthesis (µmol` N g-1 mstruct) + :param float D_Proteins: Protein degradation (µmol` N g-1 mstruct) + :param float hiddenzone_Loading_Amino_Acids_contribution: Amino acids imported from the emerged tissues (µmol` N) - :return: delta amino acids (�mol` N amino acids) + :return: delta amino acids (µmol` N amino acids) :rtype: float """ return Unloading_Amino_Acids + (D_Proteins - S_Proteins) * self.mstruct + hiddenzone_Loading_Amino_Acids_contribution @@ -642,10 +642,10 @@ def calculate_amino_acids_derivative(self, Unloading_Amino_Acids, S_Proteins, D_ def calculate_fructan_derivative(self, S_Fructan, D_Fructan): """delta fructans of hidden zone. - :param float S_Fructan: Fructans synthesis (�mol` C g-1 mstruct) - :param float D_Fructan: Fructans degradation (�mol` C g-1 mstruct) + :param float S_Fructan: Fructans synthesis (µmol` C g-1 mstruct) + :param float D_Fructan: Fructans degradation (µmol` C g-1 mstruct) - :return: delta fructans (�mol` C fructans) + :return: delta fructans (µmol` C fructans) :rtype: float """ return (S_Fructan - D_Fructan) * self.mstruct @@ -653,10 +653,10 @@ def calculate_fructan_derivative(self, S_Fructan, D_Fructan): def calculate_proteins_derivative(self, S_Proteins, D_Proteins): """delta proteins of hidden zone. - :param float S_Proteins: Protein synthesis (�mol` N g-1 mstruct) - :param float D_Proteins: Protein degradation (�mol` N g-1 mstruct) + :param float S_Proteins: Protein synthesis (µmol` N g-1 mstruct) + :param float D_Proteins: Protein degradation (µmol` N g-1 mstruct) - :return: delta proteins (�mol` N proteins) + :return: delta proteins (µmol` N proteins) :rtype: float """ return (S_Proteins - D_Proteins) * self.mstruct @@ -675,8 +675,8 @@ def __init__(self, label='phloem', sucrose=INIT_COMPARTMENTS.sucrose, amino_acid super(Phloem, self).__init__(label) # state variables - self.sucrose = sucrose #: �mol` C sucrose - self.amino_acids = amino_acids #: �mol` N amino acids + self.sucrose = sucrose #: µmol` C sucrose + self.amino_acids = amino_acids #: µmol` N amino acids # COMPARTMENTS @@ -686,7 +686,7 @@ def calculate_sucrose_derivative(contributors): :param list [PhotosyntheticOrganElement, Grains, Roots, HiddenZone, Endosperm] contributors: Organs exchanging C with the phloem - :return: delta sucrose (�mol` C sucrose) + :return: delta sucrose (µmol` C sucrose) :rtype: float """ sucrose_derivative = 0 @@ -710,7 +710,7 @@ def calculate_amino_acids_derivative(contributors): :param list [PhotosyntheticOrganElement, Grains, Roots, HiddenZone, Endosperm] contributors: Organs exchanging N with the phloem - :return: delta amino acids (�mol` N amino acids) + :return: delta amino acids (µmol` N amino acids) :rtype: float """ amino_acids_derivative = 0 @@ -746,21 +746,21 @@ def __init__(self, label='grains', age_from_flowering=INIT_COMPARTMENTS.age_from # state variables self.age_from_flowering = age_from_flowering #: seconds - self.starch = starch #: �mol` of C starch - self.structure = structure #: �mol` of C sucrose - self.proteins = proteins #: �mol` of N proteins + self.starch = starch #: µmol` of C starch + self.structure = structure #: µmol` of C sucrose + self.proteins = proteins #: µmol` of N proteins # derived attributes self.structural_dry_mass = None #: g of MS # fluxes from phloem - self.S_grain_structure = None #: current synthesis of grain structure integrated over a delta t (�mol` C) - self.S_grain_starch = None #: current synthesis of grain starch integrated over a delta t (�mol` C g-1 mstruct) - self.S_Proteins = None #: current synthesis of grain proteins integrated over a delta t (�mol` N) + self.S_grain_structure = None #: current synthesis of grain structure integrated over a delta t (µmol` C) + self.S_grain_starch = None #: current synthesis of grain starch integrated over a delta t (µmol` C g-1 mstruct) + self.S_Proteins = None #: current synthesis of grain proteins integrated over a delta t (µmol` N) # intermediate variables - self.R_grain_growth_struct = None #: grain struct respiration (�mol` C respired) - self.R_grain_growth_starch = None #: grain starch growth respiration (�mol` C respired) + self.R_grain_growth_struct = None #: grain struct respiration (µmol` C respired) + self.R_grain_growth_starch = None #: grain starch growth respiration (µmol` C respired) def initialize(self): """Initialize the derived attributes of the organ. @@ -772,7 +772,7 @@ def initialize(self): def calculate_structural_dry_mass(structure): """Grain structural dry mass. - :param float structure: Grain structural C mass (�mol` C) + :param float structure: Grain structural C mass (µmol` C) :return: Grain structural dry mass (g) :rtype: float @@ -793,7 +793,7 @@ def modified_Arrhenius_equation(temperature): # TODO: move in a separate model Temp_Ea_R = 8900 # Parameter Ea/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (K) Temp_DS_R = 68.432 # Parameter deltaS/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (dimensionless) Temp_DH_R = 20735.5 # Parameter deltaH/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (K) - Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(�C) + Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(°C) def Arrhenius_equation(T): return T * exp(-Temp_Ea_R / T) / (1 + exp(Temp_DS_R - Temp_DH_R / T)) @@ -813,9 +813,9 @@ def calculate_temperature_effect_on_growth(self, SAM_temperature): """Effect of the temperature on grain growth. Return value of equation from Johnson and Lewin (1946) for temperature. The equation is modified to return zero below zero degree. Identical to modified_Arrhenius_equation in Morphogenesis. - Should multiply the rate at 20�C + Should multiply the rate at 20°C - :param float SAM_temperature: Temperature of the Shoot Apical Meristem (�C) + :param float SAM_temperature: Temperature of the Shoot Apical Meristem (°C) :return: Correction to apply to RGR Structure of the grains (dimensionless) :rtype: float @@ -825,15 +825,15 @@ def calculate_temperature_effect_on_growth(self, SAM_temperature): # FLUXES def calculate_S_grain_structure(self, prec_structure, sucrose_phloem, mstruct_axis, T_effect_growth): - """Rate of grain structure synthesis (�mol` C structure h-1). + """Rate of grain structure synthesis (µmol` C structure h-1). Exponential function, RGR regulated by sucrose concentration in the phloem. - :param float prec_structure: Grain structure at t-1 (�mol` C) - :param float sucrose_phloem: Sucrose amount in phloem (�mol` C) + :param float prec_structure: Grain structure at t-1 (µmol` C) + :param float sucrose_phloem: Sucrose amount in phloem (µmol` C) :param float mstruct_axis: The structural dry mass of the axis (g) - :param float T_effect_growth: Effect of the temperature on the growth rate at 20�C (AU) + :param float T_effect_growth: Effect of the temperature on the growth rate at 20°C (AU) - :return: Rate of Synthesis of grain structure (�mol` C h-1) + :return: Rate of Synthesis of grain structure (µmol` C h-1) :rtype: float """ if self.age_from_flowering <= Grains.PARAMETERS.FILLING_INIT: #: Grain enlargement @@ -845,15 +845,15 @@ def calculate_S_grain_structure(self, prec_structure, sucrose_phloem, mstruct_ax return S_grain_structure def calculate_S_grain_starch(self, sucrose_phloem, mstruct_axis, T_effect_Vmax): - """Rate of starch synthesis in grains (i.e. grain filling) (�mol` C starch g-1 mstruct h-1). + """Rate of starch synthesis in grains (i.e. grain filling) (µmol` C starch g-1 mstruct h-1). Michaelis-Menten function of sucrose concentration in the phloem. - :param float sucrose_phloem: Sucrose amount in phloem (�mol` C) + :param float sucrose_phloem: Sucrose amount in phloem (µmol` C) :param float mstruct_axis: The structural dry mass of the axis (g) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Synthesis of grain starch (�mol` C g-1 mstruct h-1) + :return: Rate of Synthesis of grain starch (µmol` C g-1 mstruct h-1) :rtype: float """ if Grains.PARAMETERS.FILLING_INIT <= self.age_from_flowering < Grains.PARAMETERS.FILLING_END: #: Between grain enlargement and grain maturity @@ -868,13 +868,13 @@ def calculate_S_proteins(S_grain_structure, S_grain_starch, amino_acids_phloem, """Protein synthesis in grains. N is assumed to be co-transported along with the unloaded sucrose from phloem (using the ratio amino acids:sucrose of phloem). - :param float S_grain_structure: Synthesis of grain structure (�mol` C) - :param float S_grain_starch: Synthesis of grain starch (�mol` C g-1 mstruct) - :param float amino_acids_phloem: Amino acids concentration in phloem (�mol` N g-1 mstruct) - :param float sucrose_phloem: Sucrose concentration in phloem (�mol` C g-1 mstruct) + :param float S_grain_structure: Synthesis of grain structure (µmol` C) + :param float S_grain_starch: Synthesis of grain starch (µmol` C g-1 mstruct) + :param float amino_acids_phloem: Amino acids concentration in phloem (µmol` N g-1 mstruct) + :param float sucrose_phloem: Sucrose concentration in phloem (µmol` C g-1 mstruct) :param float structural_dry_mass: Grain structural dry mass (g) - :return: Synthesis of grain proteins (�mol` N) + :return: Synthesis of grain proteins (µmol` N) :rtype: float """ if sucrose_phloem > 0: @@ -888,10 +888,10 @@ def calculate_S_proteins(S_grain_structure, S_grain_starch, amino_acids_phloem, def calculate_structure_derivative(S_grain_structure, R_growth): """delta grain structure. - :param float S_grain_structure: Synthesis of grain structure (�mol` C) - :param float R_growth: Grain growth respiration (�mol` C respired) + :param float S_grain_structure: Synthesis of grain structure (µmol` C) + :param float R_growth: Grain growth respiration (µmol` C respired) - :return: delta grain structure (�mol` C structure) + :return: delta grain structure (µmol` C structure) :rtype: float """ return S_grain_structure - R_growth @@ -900,11 +900,11 @@ def calculate_structure_derivative(S_grain_structure, R_growth): def calculate_starch_derivative(S_grain_starch, structural_dry_mass, R_growth): """delta grain starch. - :param float S_grain_starch: Synthesis of grain starch (�mol` C g-1 mstruct) + :param float S_grain_starch: Synthesis of grain starch (µmol` C g-1 mstruct) :param float structural_dry_mass: Grain structural dry mass (g) - :param float R_growth: Grain growth respiration (�mol` C respired) + :param float R_growth: Grain growth respiration (µmol` C respired) - :return: delta grain starch (�mol` C starch) + :return: delta grain starch (µmol` C starch) :rtype: float """ return (S_grain_starch * structural_dry_mass) - R_growth @@ -913,9 +913,9 @@ def calculate_starch_derivative(S_grain_starch, structural_dry_mass, R_growth): def calculate_proteins_derivative(S_Proteins): """delta grain proteins. - :param float S_Proteins: Synthesis of grain proteins (�mol` N) + :param float S_Proteins: Synthesis of grain proteins (µmol` N) - :return: delta grain proteins (�mol` N proteins) + :return: delta grain proteins (µmol` N proteins) :rtype: float """ return S_Proteins @@ -940,9 +940,9 @@ def __init__(self, label='roots', mstruct=INIT_COMPARTMENTS.mstruct, senesced_ms self.Nstruct = Nstruct #: Structural N mass (g) # state variables - self.sucrose = sucrose #: �mol` C sucrose - self.nitrates = nitrates #: �mol` N nitrates - self.amino_acids = amino_acids #: �mol` N amino acids + self.sucrose = sucrose #: µmol` C sucrose + self.nitrates = nitrates #: µmol` N nitrates + self.amino_acids = amino_acids #: µmol` N amino acids self.cytokinins = cytokinins #: AU cytokinins # fluxes from phloem @@ -950,25 +950,25 @@ def __init__(self, label='roots', mstruct=INIT_COMPARTMENTS.mstruct, senesced_ms self.Unloading_Amino_Acids = 0. #: current Unloading of amino acids from phloem to roots # other fluxes - self.Export_Nitrates = 0. #: Total export of nitrates from roots to shoot organs integrated over a delta t (�mol` N) - self.Export_Amino_Acids = 0. #: Total export of amino acids from roots to shoot organs integrated over a delta t (�mol` N) - self.S_Amino_Acids = None #: Rate of amino acid synthesis in roots integrated over a delta t (�mol` N g-1 mstruct) - self.Uptake_Nitrates = None #: Rate of nitrate uptake by roots integrated over a delta t (�mol` N nitrates) + self.Export_Nitrates = 0. #: Total export of nitrates from roots to shoot organs integrated over a delta t (µmol` N) + self.Export_Amino_Acids = 0. #: Total export of amino acids from roots to shoot organs integrated over a delta t (µmol` N) + self.S_Amino_Acids = None #: Rate of amino acid synthesis in roots integrated over a delta t (µmol` N g-1 mstruct) + self.Uptake_Nitrates = None #: Rate of nitrate uptake by roots integrated over a delta t (µmol` N nitrates) self.S_cytokinins = None #: Rate of cytokinin synthesis integrated over a delta t (AU g-1 mstruct) self.Export_cytokinins = 0. #: Total export of cytokinin from roots to shoot organs integrated over a delta t (AU) # Integrated variables - self.Total_Organic_Nitrogen = None #: current amount of organic N (�mol` N) + self.Total_Organic_Nitrogen = None #: current amount of organic N (µmol` N) # intermediate variables - self.R_Nnit_upt = None #: Nitrate uptake respiration (�mol` C respired) - self.R_Nnit_red = None #: Nitrate reduction-linked respiration (�mol` C respired) - self.R_residual = None #: Residual maintenance respiration (cost from protein turn-over, cell ion gradients, futile cycles...) (�mol` C respired) - self.C_exudation = None #: C sucrose lost by root exudation integrated over a delta t (�mol` C g-1 mstruct) - self.N_exudation = None #: N amino acids lost by root exudation integrated over a delta t (�mol` N g-1 mstruct) + self.R_Nnit_upt = None #: Nitrate uptake respiration (µmol` C respired) + self.R_Nnit_red = None #: Nitrate reduction-linked respiration (µmol` C respired) + self.R_residual = None #: Residual maintenance respiration (cost from protein turn-over, cell ion gradients, futile cycles...) (µmol` C respired) + self.C_exudation = None #: C sucrose lost by root exudation integrated over a delta t (µmol` C g-1 mstruct) + self.N_exudation = None #: N amino acids lost by root exudation integrated over a delta t (µmol` N g-1 mstruct) self.regul_transpiration = None #: Dimensionless regulating factor of metabolite exports from roots by shoot transpiration - self.HATS_LATS = None #: Nitrate influx (�mol` N) - self.sum_respi = None #: Sum of respirations for roots i.e. related to N uptake, amino acids synthesis and residual (�mol` C) + self.HATS_LATS = None #: Nitrate influx (µmol` N) + self.sum_respi = None #: Sum of respirations for roots i.e. related to N uptake, amino acids synthesis and residual (µmol` C) def calculate_aggregated_variables(self): self.Total_Organic_Nitrogen = self.calculate_Total_Organic_Nitrogen(self.amino_acids, self.Nstruct) @@ -980,10 +980,10 @@ def calculate_Total_Organic_Nitrogen(amino_acids, Nstruct): """Total amount of organic N (amino acids + Nstruct). Used to calculate residual respiration. - :param float amino_acids: Amount of amino acids (�mol` N) + :param float amino_acids: Amount of amino acids (µmol` N) :param float Nstruct: Structural N mass (g) - :return: Total amount of organic N (�mol` N) + :return: Total amount of organic N (µmol` N) :rtype: float """ return amino_acids + (Nstruct / EcophysiologicalConstants.N_MOLAR_MASS) * 1E6 @@ -1003,26 +1003,26 @@ def calculate_regul_transpiration(total_transpiration): # FLUXES def calculate_Unloading_Sucrose(self, sucrose_roots, sucrose_phloem, mstruct_axis, T_effect_conductivity, nb_leaves): - """Rate of sucrose Unloading from phloem to roots (�mol` C sucrose unloaded g-1 mstruct h-1). + """Rate of sucrose Unloading from phloem to roots (µmol` C sucrose unloaded g-1 mstruct h-1). - :param float sucrose_roots: Amount of sucrose in roots (�mol` C) - :param float sucrose_phloem: Sucrose concentration in phloem (�mol` C g-1 mstruct) + :param float sucrose_roots: Amount of sucrose in roots (µmol` C) + :param float sucrose_phloem: Sucrose concentration in phloem (µmol` C g-1 mstruct) :param float mstruct_axis: The structural dry mass of the axis (g) - :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20�C (AU) + :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20°C (AU) :param int nb_leaves: Number of leaves - :return: Rate of Sucrose Unloading (�mol` C g-1 mstruct h-1) + :return: Rate of Sucrose Unloading (µmol` C g-1 mstruct h-1) :rtype: float """ conc_sucrose_roots = sucrose_roots / (self.mstruct * self.__class__.PARAMETERS.ALPHA) conc_sucrose_phloem = sucrose_phloem / (mstruct_axis * parameters.AXIS_PARAMETERS.ALPHA) - #: Driving compartment (�mol` C g-1 mstruct) + #: Driving compartment (µmol` C g-1 mstruct) driving_sucrose_compartment = max(conc_sucrose_roots, conc_sucrose_phloem) - #: Gradient of sucrose between the roots and the phloem (�mol` C g-1 mstruct) + #: Gradient of sucrose between the roots and the phloem (µmol` C g-1 mstruct) diff_sucrose = conc_sucrose_phloem - conc_sucrose_roots - #: Conductance depending on mstruct (g2 �mol`-1 s-1) + #: Conductance depending on mstruct (g2 µmol`-1 s-1) SIGMA_SUCROSE = max(Roots.PARAMETERS.SIGMA_SUCROSE_MIN, ((Roots.PARAMETERS.SIGMA_SUCROSE_MAX * Roots.PARAMETERS.SIGMA_SUCROSE_K ** Roots.PARAMETERS.SIGMA_SUCROSE_N) / (max(0, nb_leaves ** Roots.PARAMETERS.SIGMA_SUCROSE_N) + Roots.PARAMETERS.SIGMA_SUCROSE_K ** Roots.PARAMETERS.SIGMA_SUCROSE_N))) @@ -1035,11 +1035,11 @@ def calculate_Unloading_Amino_Acids(self, amino_acids_roots, amino_acids_phloem, """Unloading of amino_acids from phloem to roots. Amino acids are assumed to be co-transported along with the unloaded sucrose from phloem (using the ratio amino acids:sucrose of phloem). - :param float Unloading_Sucrose: Sucrose Unloading (�mol` C g-1 mstruct) - :param float sucrose_phloem: Sucrose concentration in phloem (�mol` C g-1 mstruct) - :param float amino_acids_phloem: Amino acids concentration in phloem (�mol` N g-1 mstruct) + :param float Unloading_Sucrose: Sucrose Unloading (µmol` C g-1 mstruct) + :param float sucrose_phloem: Sucrose concentration in phloem (µmol` C g-1 mstruct) + :param float amino_acids_phloem: Amino acids concentration in phloem (µmol` N g-1 mstruct) - :return: Amino acids Unloading (�mol` N g-1 mstruct) + :return: Amino acids Unloading (µmol` N g-1 mstruct) :rtype: float """ #: todo: test to reimplement the option below where the unloading of AA is proportional to that of sucrose @@ -1051,11 +1051,11 @@ def calculate_Unloading_Amino_Acids(self, amino_acids_roots, amino_acids_phloem, conc_amino_acids_roots = amino_acids_roots / (self.mstruct * self.__class__.PARAMETERS.ALPHA) conc_amino_acids_phloem = amino_acids_phloem / (mstruct_axis * parameters.AXIS_PARAMETERS.ALPHA) - #: Driving compartment (�mol` N g-1 mstruct) + #: Driving compartment (µmol` N g-1 mstruct) driving_amino_acids_compartment = max(conc_amino_acids_roots, conc_amino_acids_phloem) - #: Gradient of sucrose between the roots and the phloem (�mol` C g-1 mstruct) + #: Gradient of sucrose between the roots and the phloem (µmol` C g-1 mstruct) diff_amino_acids = conc_amino_acids_phloem - conc_amino_acids_roots - #: Conductance depending on mstruct (g2 �mol`-1 s-1) + #: Conductance depending on mstruct (g2 µmol`-1 s-1) conductance = Roots.PARAMETERS.SIGMA_AMINO_ACIDS * Roots.PARAMETERS.BETA * self.mstruct ** (2 / 3) * T_effect_conductivity return driving_amino_acids_compartment * diff_amino_acids * conductance * parameters.SECOND_TO_HOUR_RATE_CONVERSION @@ -1066,30 +1066,30 @@ def calculate_Uptake_Nitrates(self, Conc_Nitrates_Soil, nitrates_roots, sucrose_ - HATS and LATS parameters are calculated as a function of root nitrate concentration (negative regulation) - Nitrate uptake is finally regulated by sucrose concentration (positive regulation) - :param float Conc_Nitrates_Soil: Soil nitrate concentration Unloading (�mol` N m-3 soil) - :param float nitrates_roots: Amount of nitrates in roots (�mol` N) - :param float sucrose_roots: Amount of sucrose in roots (�mol` C) + :param float Conc_Nitrates_Soil: Soil nitrate concentration Unloading (µmol` N m-3 soil) + :param float nitrates_roots: Amount of nitrates in roots (µmol` N) + :param float sucrose_roots: Amount of sucrose in roots (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity :param float SRWC: Soil Relative Water Content (%) - :return: Nitrate uptake (�mol` N nitrates) and nitrate influxes HATS and LATS (�mol` N h-1) + :return: Nitrate uptake (µmol` N nitrates) and nitrate influxes HATS and LATS (µmol` N h-1) :rtype: (float, float) """ conc_nitrates_roots = nitrates_roots / self.mstruct #: High Affinity Transport System (HATS) VMAX_HATS_MAX = max(0., - Roots.PARAMETERS.A_VMAX_HATS * conc_nitrates_roots + Roots.PARAMETERS.B_VMAX_HATS) #: Maximal rate of nitrates influx at saturating soil N concentration;HATS (�mol` N nitrates g-1 mstruct s-1) + Roots.PARAMETERS.A_VMAX_HATS * conc_nitrates_roots + Roots.PARAMETERS.B_VMAX_HATS) #: Maximal rate of nitrates influx at saturating soil N concentration;HATS (µmol` N nitrates g-1 mstruct s-1) K_HATS = max(0., - Roots.PARAMETERS.A_K_HATS * conc_nitrates_roots + Roots.PARAMETERS.B_K_HATS) #: Affinity coefficient of nitrates influx at saturating soil N concentration;HATS (�mol` m-3) - HATS = (VMAX_HATS_MAX * Conc_Nitrates_Soil) / (K_HATS + Conc_Nitrates_Soil) #: Rate of nitrate influx by HATS (�mol` N nitrates uptake s-1 g-1 mstruct) + Roots.PARAMETERS.A_K_HATS * conc_nitrates_roots + Roots.PARAMETERS.B_K_HATS) #: Affinity coefficient of nitrates influx at saturating soil N concentration;HATS (µmol` m-3) + HATS = (VMAX_HATS_MAX * Conc_Nitrates_Soil) / (K_HATS + Conc_Nitrates_Soil) #: Rate of nitrate influx by HATS (µmol` N nitrates uptake s-1 g-1 mstruct) #: Low Affinity Transport System (LATS) K_LATS = max(0., Roots.PARAMETERS.A_LATS * conc_nitrates_roots + Roots.PARAMETERS.B_LATS) #: Rate constant for nitrates influx at low soil N concentration; LATS (m3 g-1 mstruct s-1) - LATS = (K_LATS * Conc_Nitrates_Soil) #: Rate of nitrate influx by LATS (�mol` N nitrates g-1 mstruct) + LATS = (K_LATS * Conc_Nitrates_Soil) #: Rate of nitrate influx by LATS (µmol` N nitrates g-1 mstruct) - #: Nitrate influx (�mol` N) + #: Nitrate influx (µmol` N) HATS_LATS = (HATS + LATS) nitrate_influx = HATS_LATS * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax * self.mstruct @@ -1100,19 +1100,19 @@ def calculate_Uptake_Nitrates(self, Conc_Nitrates_Soil, nitrates_roots, sucrose_ if HATS_LATS < Roots.PARAMETERS.MIN_INFLUX_FOR_UPTAKE: net_nitrate_uptake = 0 else: - net_nitrate_uptake = nitrate_influx * Roots.PARAMETERS.NET_INFLUX_UPTAKE_RATIO * regul_C * regul_W #: Net nitrate uptake (�mol` N nitrates uptake by roots) + net_nitrate_uptake = nitrate_influx * Roots.PARAMETERS.NET_INFLUX_UPTAKE_RATIO * regul_C * regul_W #: Net nitrate uptake (µmol` N nitrates uptake by roots) return net_nitrate_uptake, nitrate_influx def calculate_S_amino_acids(self, nitrates, sucrose, T_effect_Vmax): - """Rate of amino acid synthesis in roots (�mol` N amino acids g-1 mstruct h-1). + """Rate of amino acid synthesis in roots (µmol` N amino acids g-1 mstruct h-1). Bi-substrate Michaelis-Menten function of nitrates and sucrose. - :param float nitrates: Amount of nitrates in roots (�mol` N) - :param float sucrose: Amount of sucrose in roots (�mol` C) + :param float nitrates: Amount of nitrates in roots (µmol` N) + :param float sucrose: Amount of sucrose in roots (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Amino acids synthesis (�mol` N g-1 mstruct h-1) + :return: Amino acids synthesis (µmol` N g-1 mstruct h-1) :rtype: float """ return T_effect_Vmax * Roots.PARAMETERS.VMAX_AMINO_ACIDS / ((1 + Roots.PARAMETERS.K_AMINO_ACIDS_NITRATES / (nitrates / (self.mstruct * Roots.PARAMETERS.ALPHA))) * @@ -1123,51 +1123,51 @@ def calculate_Export_Nitrates(self, nitrates, regul_transpiration): """Total export of nitrates from roots to shoot organs Export is calculated as a function on nitrate concentration and culm transpiration. - :param float nitrates: Amount of nitrates in roots (�mol` N) + :param float nitrates: Amount of nitrates in roots (µmol` N) :param float regul_transpiration: Regulating factor by transpiration (mmol H2O m-2 s-1) - :return: Rate of Export of nitrates (�mol` N h-1) + :return: Rate of Export of nitrates (µmol` N h-1) :rtype: float """ - f_nitrates = (nitrates / (self.mstruct * Roots.PARAMETERS.ALPHA)) * Roots.PARAMETERS.K_NITRATE_EXPORT #: �mol` g-1 s-1 - Export_Nitrates = f_nitrates * self.mstruct * regul_transpiration * parameters.SECOND_TO_HOUR_RATE_CONVERSION #: Nitrate export regulation by transpiration (�mol` N) + f_nitrates = (nitrates / (self.mstruct * Roots.PARAMETERS.ALPHA)) * Roots.PARAMETERS.K_NITRATE_EXPORT #: µmol` g-1 s-1 + Export_Nitrates = f_nitrates * self.mstruct * regul_transpiration * parameters.SECOND_TO_HOUR_RATE_CONVERSION #: Nitrate export regulation by transpiration (µmol` N) return max(min(Export_Nitrates, nitrates), 0.) def calculate_Export_Amino_Acids(self, amino_acids, regul_transpiration): """Total export of amino acids from roots to shoot organs Amino acids export is calculated as a function of nitrate export using the ratio amino acids:nitrates in roots. - :param float amino_acids: Amount of amino acids in roots (�mol` N) + :param float amino_acids: Amount of amino acids in roots (µmol` N) :param float regul_transpiration: Regulating factor by transpiration (mmol H2O m-2 s-1) :Returns: - Rate of Export of amino acids (�mol` N h-1) + Rate of Export of amino acids (µmol` N h-1) :Returns Type: :class:`float` """ - f_amino_acids = (amino_acids / (self.mstruct * Roots.PARAMETERS.ALPHA)) * Roots.PARAMETERS.K_AMINO_ACIDS_EXPORT #: �mol` g-1 s-1 - Export_Amino_Acids = f_amino_acids * self.mstruct * regul_transpiration * parameters.SECOND_TO_HOUR_RATE_CONVERSION #: Amino acids export regulation by plant transpiration (�mol` N) + f_amino_acids = (amino_acids / (self.mstruct * Roots.PARAMETERS.ALPHA)) * Roots.PARAMETERS.K_AMINO_ACIDS_EXPORT #: µmol` g-1 s-1 + Export_Amino_Acids = f_amino_acids * self.mstruct * regul_transpiration * parameters.SECOND_TO_HOUR_RATE_CONVERSION #: Amino acids export regulation by plant transpiration (µmol` N) return max(min(Export_Amino_Acids, amino_acids), 0.) @staticmethod def calculate_exudation(Unloading_Sucrose, sucrose_roots, amino_acids_roots, amino_acids_phloem): - """C sucrose and N amino acids lost by root exudation (�mol` C or N g-1 mstruct). + """C sucrose and N amino acids lost by root exudation (µmol` C or N g-1 mstruct). - C exudation is calculated as a fraction of C Unloading from phloem - N exudation is calculated from C exudation using the ratio amino acids:sucrose of the phloem - :param float Unloading_Sucrose: Sucrose Unloading (�mol` C g-1 mstruct h-1) - :param float sucrose_roots: Amount of sucrose in roots (�mol` C) - :param float amino_acids_roots: Amount of amino acids in roots (�mol` N) - :param float amino_acids_phloem: Amount of amino acids in phloem (�mol` N) + :param float Unloading_Sucrose: Sucrose Unloading (µmol` C g-1 mstruct h-1) + :param float sucrose_roots: Amount of sucrose in roots (µmol` C) + :param float amino_acids_roots: Amount of amino acids in roots (µmol` N) + :param float amino_acids_phloem: Amount of amino acids in phloem (µmol` N) - :return: Rates of C exuded (�mol` C g-1 mstruct h-1) and N_exudation (�mol` N g-1 mstruct h-1) + :return: Rates of C exuded (µmol` C g-1 mstruct h-1) and N_exudation (µmol` N g-1 mstruct h-1) :rtype: (float, float) """ if sucrose_roots <= 0 or Unloading_Sucrose <= 0: C_exudation = 0 else: - C_exudation = min(sucrose_roots, Unloading_Sucrose * Roots.PARAMETERS.C_EXUDATION) #: C exuded (�mol` g-1 mstruct) + C_exudation = min(sucrose_roots, Unloading_Sucrose * Roots.PARAMETERS.C_EXUDATION) #: C exuded (µmol` g-1 mstruct) if amino_acids_phloem <= 0 or amino_acids_roots <= 0 or sucrose_roots <= 0: N_exudation = 0 else: @@ -1179,8 +1179,8 @@ def calculate_S_cytokinins(self, sucrose_roots, nitrates_roots, T_effect_Vmax): Cytokinin synthesis regulated by both root sucrose and nitrates. As a signal molecule, cytokinins are assumed to have a neglected effect on sucrose. Thus, no cost in C is applied to the sucrose pool. - :param float sucrose_roots: Amount of sucrose in roots (�mol` C) - :param float nitrates_roots: Amount of nitrates in roots (�mol` N) + :param float sucrose_roots: Amount of sucrose in roots (µmol` C) + :param float nitrates_roots: Amount of nitrates in roots (µmol` N) :param float T_effect_Vmax: Correction to apply to enzyme activity :return: Rate of Cytokinin synthesis (AU g-1 mstruct h-1) @@ -1216,12 +1216,12 @@ def calculate_Export_cytokinins(self, cytokinins, regul_transpiration): def calculate_sucrose_derivative(self, Unloading_Sucrose, S_Amino_Acids, C_exudation, sum_respi): """delta root sucrose. - :param float Unloading_Sucrose: Sucrose Unloading (�mol` C g-1 mstruct) - :param float S_Amino_Acids: Amino acids synthesis (�mol` N g-1 mstruct) - :param float C_exudation: C exudation (�mol` C g-1 mstruct) - :param float sum_respi: Sum of respirations for roots i.e. related to N uptake, amino acids synthesis and residual (�mol` C) + :param float Unloading_Sucrose: Sucrose Unloading (µmol` C g-1 mstruct) + :param float S_Amino_Acids: Amino acids synthesis (µmol` N g-1 mstruct) + :param float C_exudation: C exudation (µmol` C g-1 mstruct) + :param float sum_respi: Sum of respirations for roots i.e. related to N uptake, amino acids synthesis and residual (µmol` C) - :return: delta root sucrose (�mol` C sucrose) + :return: delta root sucrose (µmol` C sucrose) :rtype: float """ #: Contribution of sucrose to the synthesis of amino_acids @@ -1231,11 +1231,11 @@ def calculate_sucrose_derivative(self, Unloading_Sucrose, S_Amino_Acids, C_exuda def calculate_nitrates_derivative(self, Uptake_Nitrates, Export_Nitrates, S_Amino_Acids): """delta root nitrates. - :param float Uptake_Nitrates: Nitrate uptake (�mol` N nitrates) - :param float Export_Nitrates: Export of nitrates (�mol` N) - :param float S_Amino_Acids: Amino acids synthesis (�mol` N g-1 mstruct) + :param float Uptake_Nitrates: Nitrate uptake (µmol` N nitrates) + :param float Export_Nitrates: Export of nitrates (µmol` N) + :param float S_Amino_Acids: Amino acids synthesis (µmol` N g-1 mstruct) - :return: delta root nitrates (�mol` N nitrates) + :return: delta root nitrates (µmol` N nitrates) :rtype: float """ import_nitrates_roots = Uptake_Nitrates @@ -1245,12 +1245,12 @@ def calculate_nitrates_derivative(self, Uptake_Nitrates, Export_Nitrates, S_Amin def calculate_amino_acids_derivative(self, Unloading_Amino_Acids, S_Amino_Acids, Export_Amino_Acids, N_exudation): """delta root amino acids. - :param float Unloading_Amino_Acids: Amino acids Unloading (�mol` N g-1 mstruct) - :param float S_Amino_Acids: Amino acids synthesis (�mol` N g-1 mstruct) - :param float Export_Amino_Acids: Export of amino acids (�mol` N) - :param float N_exudation: N exuded (�mol` g-1 mstruct) + :param float Unloading_Amino_Acids: Amino acids Unloading (µmol` N g-1 mstruct) + :param float S_Amino_Acids: Amino acids synthesis (µmol` N g-1 mstruct) + :param float Export_Amino_Acids: Export of amino acids (µmol` N) + :param float N_exudation: N exuded (µmol` g-1 mstruct) - :return: delta root amino acids (�mol` N amino acids) + :return: delta root amino acids (µmol` N amino acids) :rtype: float """ return (Unloading_Amino_Acids + S_Amino_Acids - N_exudation) * self.mstruct - Export_Amino_Acids @@ -1295,7 +1295,7 @@ def __init__(self, label, exposed_element, enclosed_element): self.enclosed_element = enclosed_element #: the enclosed element self.mstruct = None #: the structural dry mass self.senesced_mstruct = None #: senesced structural dry mass - self.nitrates = None #: nitrates (�mol N) + self.nitrates = None #: nitrates (µmol N) def calculate_aggregated_variables(self): self.mstruct = 0 @@ -1382,9 +1382,9 @@ def __init__(self, label=None, green_area=INIT_COMPARTMENTS.green_area, mstruct= Tr=INIT_COMPARTMENTS.Tr, Ag=INIT_COMPARTMENTS.Ag, Ts=INIT_COMPARTMENTS.Ts, is_growing=INIT_COMPARTMENTS.is_growing, cohorts=None, cohorts_replications=None, index=None): self.label = label #: the label of the element - if cohorts is None: #: list of cohort values - TODO: Hack to treat tillering cases : TEMPORARY. Devrait �tre port� � l'�chelle de la plante uniquement mais je ne vois pas comment faire mieux + if cohorts is None: #: list of cohort values - TODO: Hack to treat tillering cases : TEMPORARY. Devrait être porté à l'échelle de la plante uniquement mais je ne vois pas comment faire mieux cohorts = [] - # TODO: Hack to deal with tillering cases: TEMPORARY.Devrait �tre port� � l'�chelle de la plante uniquement mais je ne vois pas comment faire mieux + # TODO: Hack to deal with tillering cases: TEMPORARY.Devrait être porté à l'échelle de la plante uniquement mais je ne vois pas comment faire mieux self.cohorts = cohorts #: list of cohort values self.cohorts_replications = cohorts_replications #: dictionary of number of replications per cohort rank self.index = index #: the index of the phytomer TEMPORARY @@ -1396,48 +1396,48 @@ def __init__(self, label=None, green_area=INIT_COMPARTMENTS.green_area, mstruct= self.is_growing = is_growing #: Flag indicating if the element is growing or not (:class:`bool`) self.green_area = green_area #: green area (m-2) self.Tr = Tr #: Transpiration rate (mmol m-2 s-1) - self.Ag = Ag #: Gross assimilation (�mol` m-2 s-1) - self.Ts = Ts #: Organ temperature (�C) + self.Ag = Ag #: Gross assimilation (µmol` m-2 s-1) + self.Ts = Ts #: Organ temperature (°C) # state variables - self.triosesP = triosesP #: �mol` C - self.starch = starch #: �mol` C - self.sucrose = sucrose #: �mol` C - self.fructan = fructan #: �mol` C - self.nitrates = nitrates #: �mol` N - self.amino_acids = amino_acids #: �mol` N - self.proteins = proteins #: �mol` N + self.triosesP = triosesP #: µmol` C + self.starch = starch #: µmol` C + self.sucrose = sucrose #: µmol` C + self.fructan = fructan #: µmol` C + self.nitrates = nitrates #: µmol` N + self.amino_acids = amino_acids #: µmol` N + self.proteins = proteins #: µmol` N self.cytokinins = cytokinins #: AU # fluxes to phloem - self.Loading_Sucrose = None #: Rate of sucrose loading to phloem (�mol` C) - self.Loading_Amino_Acids = None #: Rate of amino acids loading to phloem (�mol` N) + self.Loading_Sucrose = None #: Rate of sucrose loading to phloem (µmol` C) + self.Loading_Amino_Acids = None #: Rate of amino acids loading to phloem (µmol` N) # other fluxes - self.S_Proteins = None #: Rate of protein synthesis (�mol` N g-1 mstruct) - self.S_Amino_Acids = None #: Rate of amino acids synthesis (�mol` N g-1 mstruct) - self.Regul_S_Fructan = None #: Maximal rate of fructan synthesis (�mol` C g-1 mstruct) - self.S_Starch = None #: Rate of starch synthesis (�mol` C g-1 mstruct) - self.D_Starch = None #: Rate of starch degradation (�mol` C g-1 mstruct) - self.S_Sucrose = None #: Rate of sucrose synthesis (�mol` C g-1 mstruct) - self.S_Fructan = None #: Rate of fructan synthesis (�mol` C g-1 mstruct) - self.D_Fructan = None #: Rate of fructan degradation ((�mol` C g-1 mstruct) - self.Nitrates_import = None #: Total nitrates imported from roots (�mol` N nitrates) - self.Amino_Acids_import = None #: Total amino acids imported from roots (�mol` N amino acids) - self.D_Proteins = None #: Rate of protein degradation (�mol` N g-1 mstruct) + self.S_Proteins = None #: Rate of protein synthesis (µmol` N g-1 mstruct) + self.S_Amino_Acids = None #: Rate of amino acids synthesis (µmol` N g-1 mstruct) + self.Regul_S_Fructan = None #: Maximal rate of fructan synthesis (µmol` C g-1 mstruct) + self.S_Starch = None #: Rate of starch synthesis (µmol` C g-1 mstruct) + self.D_Starch = None #: Rate of starch degradation (µmol` C g-1 mstruct) + self.S_Sucrose = None #: Rate of sucrose synthesis (µmol` C g-1 mstruct) + self.S_Fructan = None #: Rate of fructan synthesis (µmol` C g-1 mstruct) + self.D_Fructan = None #: Rate of fructan degradation ((µmol` C g-1 mstruct) + self.Nitrates_import = None #: Total nitrates imported from roots (µmol` N nitrates) + self.Amino_Acids_import = None #: Total amino acids imported from roots (µmol` N amino acids) + self.D_Proteins = None #: Rate of protein degradation (µmol` N g-1 mstruct) self.cytokinins_import = None #: Import of cytokinins (AU) self.D_cytokinins = None #: Rate of cytokinins degradation (AU g-1 mstruct) # Integrated variables - self.Total_Organic_Nitrogen = None #: current total nitrogen amount (�mol` N) + self.Total_Organic_Nitrogen = None #: current total nitrogen amount (µmol` N) # intermediate variables - self.R_Nnit_red = None #: Nitrate reduction-linked respiration (�mol` C respired) - self.R_residual = None #: Residual maintenance respiration (cost from protein turn-over, cell ion gradients, futile cycles...) (�mol` C respired) + self.R_Nnit_red = None #: Nitrate reduction-linked respiration (µmol` C respired) + self.R_residual = None #: Residual maintenance respiration (cost from protein turn-over, cell ion gradients, futile cycles...) (µmol` C respired) self.Transpiration = None #: Surfacic transpiration rate of an element (mmol H2O s-1) - self.R_phloem_loading = None #: Phloem loading respiration (�mol` C respired) - self.Photosynthesis = None #: Total Photosynthesis of an element integrated over a delta t (�mol` C) - self.sum_respi = None #: Sum of respirations for the element i.e. related to C loading to phloem, amino acids synthesis and residual (�mol` C) + self.R_phloem_loading = None #: Phloem loading respiration (µmol` C respired) + self.Photosynthesis = None #: Total Photosynthesis of an element integrated over a delta t (µmol` C) + self.sum_respi = None #: Sum of respirations for the element i.e. related to C loading to phloem, amino acids synthesis and residual (µmol` C) @property def nb_replications(self): @@ -1452,12 +1452,12 @@ def calculate_aggregated_variables(self): @staticmethod def calculate_total_Photosynthesis(Ag, green_area): - """Total Photosynthesis of an element (�mol` C m-2 h-1 * m2). + """Total Photosynthesis of an element (µmol` C m-2 h-1 * m2). - :param float Ag: Gross Photosynthesis rate (�mol` C m-2 s-1) + :param float Ag: Gross Photosynthesis rate (µmol` C m-2 s-1) :param float green_area: Green area (m2) - :return: Rate of Total Photosynthesis (�mol` C h-1) + :return: Rate of Total Photosynthesis (µmol` C h-1) :rtype: float """ return Ag * green_area * parameters.SECOND_TO_HOUR_RATE_CONVERSION @@ -1476,11 +1476,11 @@ def calculate_Total_Transpiration(Tr, green_area): def calculate_Regul_S_Fructan(self, Loading_Sucrose): """Regulating function for fructan maximal rate of synthesis. - Negative regulation by the loading of sucrose from the phloem ("switch-off" sigmo�dal kinetic). + Negative regulation by the loading of sucrose from the phloem ("switch-off" sigmoïdal kinetic). - :param float Loading_Sucrose: Sucrose loading (�mol` C) + :param float Loading_Sucrose: Sucrose loading (µmol` C) - :return: Maximal rate of fructan synthesis (�mol` C g-1 mstruct h-1) + :return: Maximal rate of fructan synthesis (µmol` C g-1 mstruct h-1) :rtype: float """ # Regulation by sucrose loading -- see HiddenZone.calculate_Regul_S_Fructan for why the hard branch on @@ -1497,11 +1497,11 @@ def calculate_Total_Organic_Nitrogen(amino_acids, proteins, Nstruct): """Total amount of organic N (amino acids + proteins + Nstruct). Used to calculate residual respiration. - :param float amino_acids: Amount of amino acids (�mol` N) - :param float proteins: Amount of proteins (�mol` N) + :param float amino_acids: Amount of amino acids (µmol` N) + :param float proteins: Amount of proteins (µmol` N) :param float Nstruct: Structural N mass (g) - :return: Total amount of organic N (�mol` N) + :return: Total amount of organic N (µmol` N) :rtype: float """ return amino_acids + proteins + (Nstruct / EcophysiologicalConstants.N_MOLAR_MASS) * 1E6 @@ -1509,13 +1509,13 @@ def calculate_Total_Organic_Nitrogen(amino_acids, proteins, Nstruct): # FLUXES def calculate_S_Starch(self, triosesP, T_effect_Vmax): - """Rate of starch synthesis (�mol` C starch g-1 mstruct h-1). + """Rate of starch synthesis (µmol` C starch g-1 mstruct h-1). Michaelis-Menten function of triose phosphates. - :param float triosesP: Amount of triose phosphates (�mol` C) + :param float triosesP: Amount of triose phosphates (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Starch synthesis (�mol` C g-1 mstruct h-1) + :return: Rate of Starch synthesis (µmol` C g-1 mstruct h-1) :rtype: float """ triosesP_eff = _smooth_positive(triosesP) @@ -1524,25 +1524,25 @@ def calculate_S_Starch(self, triosesP, T_effect_Vmax): return S_Starch def calculate_D_Starch(self, starch, T_effect_Vmax): - """Rate of starch degradation (�mol` C starch g-1 mstruct h-1). + """Rate of starch degradation (µmol` C starch g-1 mstruct h-1). First order kinetic. - :param float starch: Amount of starch (�mol` C) + :param float starch: Amount of starch (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Starch degradation (�mol` C g-1 mstruct h-1) + :return: Starch degradation (µmol` C g-1 mstruct h-1) :rtype: float """ return max(0, self.__class__.PARAMETERS.DELTA_DSTARCH * (starch / (self.mstruct * self.__class__.PARAMETERS.ALPHA))) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax def calculate_S_Sucrose(self, triosesP, T_effect_Vmax): - """Rate of sucrose synthesis (�mol` C sucrose g-1 mstruct h-1). + """Rate of sucrose synthesis (µmol` C sucrose g-1 mstruct h-1). Michaelis-Menten function of triose phosphates. - :param float triosesP: Amount of triose phosphates (�mol` C) + :param float triosesP: Amount of triose phosphates (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Sucrose synthesis (�mol` C g-1 mstruct h-1) + :return: Rate of Sucrose synthesis (µmol` C g-1 mstruct h-1) :rtype: float """ triosesP_eff = _smooth_positive(triosesP) @@ -1551,44 +1551,44 @@ def calculate_S_Sucrose(self, triosesP, T_effect_Vmax): return S_Sucrose def calculate_Loading_Sucrose(self, sucrose, sucrose_phloem, mstruct_axis, T_effect_conductivity): - """Rate of sucrose loading to phloem (�mol` C sucrose h-1). + """Rate of sucrose loading to phloem (µmol` C sucrose h-1). Transport-resistance model. - :param float sucrose: Amount of sucrose in the element (�mol` C) - :param float sucrose_phloem: Amount of sucrose in the phloem (�mol` C) + :param float sucrose: Amount of sucrose in the element (µmol` C) + :param float sucrose_phloem: Amount of sucrose in the phloem (µmol` C) :param float mstruct_axis: Structural dry mass of the axis (g) - :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20�C (AU) + :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20°C (AU) - :return: Rate of Sucrose loading (�mol` C h-1) + :return: Rate of Sucrose loading (µmol` C h-1) :rtype: float """ conc_sucrose_element = sucrose / (self.mstruct * self.__class__.PARAMETERS.ALPHA) conc_sucrose_phloem = sucrose_phloem / (mstruct_axis * parameters.AXIS_PARAMETERS.ALPHA) - #: Driving compartment (�mol` C g-1 mstruct) + #: Driving compartment (µmol` C g-1 mstruct) driving_sucrose_compartment = max(conc_sucrose_element, conc_sucrose_phloem) - #: Gradient of sucrose between the element and the phloem (�mol` C g-1 mstruct) + #: Gradient of sucrose between the element and the phloem (µmol` C g-1 mstruct) diff_sucrose = conc_sucrose_element - conc_sucrose_phloem - #: Conductance depending on mstruct (g2 �mol`-1 s-1) + #: Conductance depending on mstruct (g2 µmol`-1 s-1) conductance = self.__class__.PARAMETERS.SIGMA_SUCROSE * self.__class__.PARAMETERS.BETA * self.mstruct ** (2 / 3) * T_effect_conductivity return driving_sucrose_compartment * diff_sucrose * conductance * parameters.SECOND_TO_HOUR_RATE_CONVERSION def calculate_export_sucrose(self, sucrose, sucrose_hiddenzone, mstruct_hiddenzone, T_effect_conductivity): - """Rate of sucrose exportation to hidden zone (�mol` C sucrose h-1). + """Rate of sucrose exportation to hidden zone (µmol` C sucrose h-1). Transport-resistance model. - :param float sucrose: Amount of sucrose in the element (�mol` C) - :param float sucrose_hiddenzone: Sucrose amount in the hidden zone (�mol` C) + :param float sucrose: Amount of sucrose in the element (µmol` C) + :param float sucrose_hiddenzone: Sucrose amount in the hidden zone (µmol` C) :param float mstruct_hiddenzone: mstruct of the hidden zone (g) - :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20�C (AU) + :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20°C (AU) - :return: Rate of Sucrose export (�mol` C h-1) + :return: Rate of Sucrose export (µmol` C h-1) :rtype: float """ conc_sucrose_element = sucrose / (self.mstruct * self.__class__.PARAMETERS.ALPHA) conc_sucrose_hiddenzone = sucrose_hiddenzone / mstruct_hiddenzone - #: Gradient of sucrose between the element and the hidden zone (�mol` C g-1 mstruct) + #: Gradient of sucrose between the element and the hidden zone (µmol` C g-1 mstruct) diff_sucrose = conc_sucrose_element - conc_sucrose_hiddenzone #: Conductance depending on mstruct conductance = HiddenZone.PARAMETERS.SIGMA * self.__class__.PARAMETERS.BETA * mstruct_hiddenzone ** (2 / 3) * T_effect_conductivity @@ -1596,28 +1596,28 @@ def calculate_export_sucrose(self, sucrose, sucrose_hiddenzone, mstruct_hiddenzo return diff_sucrose * conductance * parameters.SECOND_TO_HOUR_RATE_CONVERSION def calculate_S_Fructan(self, sucrose, Regul_S_Fructan, T_effect_Vmax): - """Rate of fructan synthesis (�mol` C fructan g-1 mstruct h-1). - Sigmo�dal function of sucrose. + """Rate of fructan synthesis (µmol` C fructan g-1 mstruct h-1). + Sigmoïdal function of sucrose. - :param float sucrose: Amount of sucrose (�mol` C) - :param float Regul_S_Fructan: Maximal rate of fructan synthesis regulated by sucrose loading (�mol` C g-1 mstruct) + :param float sucrose: Amount of sucrose (µmol` C) + :param float Regul_S_Fructan: Maximal rate of fructan synthesis regulated by sucrose loading (µmol` C g-1 mstruct) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Fructan synthesis (�mol` C g-1 mstruct h-1) + :return: Rate of Fructan synthesis (µmol` C g-1 mstruct h-1) :rtype: float """ return ((max(0., sucrose) / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) * Regul_S_Fructan) / \ ((max(0., sucrose) / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) + self.__class__.PARAMETERS.K_SFRUCTAN) * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax def calculate_D_Fructan(self, sucrose, fructan, T_effect_Vmax): - """Rate of fructan degradation (�mol` C fructan g-1 mstruct h-1). + """Rate of fructan degradation (µmol` C fructan g-1 mstruct h-1). Inhibition function by the end product i.e. sucrose (Bancal et al., 2012). - :param float sucrose: Amount of sucrose (�mol` C) - :param float fructan: Amount of fructan (�mol` C) + :param float sucrose: Amount of sucrose (µmol` C) + :param float fructan: Amount of fructan (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Fructan degradation (�mol` C g-1 mstruct h-1) + :return: Rate of Fructan degradation (µmol` C g-1 mstruct h-1) :rtype: float """ d_potential = ((self.__class__.PARAMETERS.K_DFRUCTAN * self.__class__.PARAMETERS.VMAX_DFRUCTAN) / @@ -1627,14 +1627,14 @@ def calculate_D_Fructan(self, sucrose, fructan, T_effect_Vmax): @staticmethod def calculate_Nitrates_import(Export_Nitrates, element_transpiration, Total_Transpiration): - """Total nitrates imported from roots (�mol` N nitrates). + """Total nitrates imported from roots (µmol` N nitrates). Nitrates coming from roots (fraction of uptake + direct export) are distributed according to the contribution of the element to culm transpiration. - :param float Export_Nitrates: Exported nitrates by roots (�mol` N) + :param float Export_Nitrates: Exported nitrates by roots (µmol` N) :param float element_transpiration: Element transpiration (mmol H2O s-1) :param float Total_Transpiration: Culm transpiration (mmol H2O s-1) - :return: Total nitrates import (�mol` N nitrates) + :return: Total nitrates import (µmol` N nitrates) :rtype: float """ if Total_Transpiration > 0: @@ -1645,14 +1645,14 @@ def calculate_Nitrates_import(Export_Nitrates, element_transpiration, Total_Tran @staticmethod def calculate_Amino_Acids_import(roots_exported_amino_acids, element_transpiration, Total_Transpiration): - """Total amino acids imported from roots (�mol` N amino acids). + """Total amino acids imported from roots (µmol` N amino acids). Amino acids exported by roots are distributed according to the contribution of the element to culm transpiration. - :param float roots_exported_amino_acids: Exported amino acids by roots (�mol` N) + :param float roots_exported_amino_acids: Exported amino acids by roots (µmol` N) :param float element_transpiration: Element transpiration (mmol H2O s-1) :param float Total_Transpiration: Culm transpiration (mmol H2O s-1) - :return: Total amino acids import (�mol` N amino acids) + :return: Total amino acids import (µmol` N amino acids) :rtype: float """ if Total_Transpiration > 0: @@ -1662,14 +1662,14 @@ def calculate_Amino_Acids_import(roots_exported_amino_acids, element_transpirati return Amino_Acids_import def calculate_S_amino_acids(self, nitrates, triosesP, T_effect_Vmax): - """Rate of amino acids synthesis (�mol` N amino acids h-1 g-1 MS). + """Rate of amino acids synthesis (µmol` N amino acids h-1 g-1 MS). Bi-substrate Michaelis-Menten function of nitrates and triose phosphates. - :param float nitrates: Amount of nitrates (�mol` N) - :param float triosesP: Amount of triosesP (�mol` C) + :param float nitrates: Amount of nitrates (µmol` N) + :param float triosesP: Amount of triosesP (µmol` C) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Amino acids synthesis (�mol` N h-1 g-1 mstruct) + :return: Rate of Amino acids synthesis (µmol` N h-1 g-1 mstruct) :rtype: float """ nitrates_eff = _smooth_positive(nitrates) @@ -1681,13 +1681,13 @@ def calculate_S_amino_acids(self, nitrates, triosesP, T_effect_Vmax): return calculate_S_amino_acids def calculate_S_proteins(self, amino_acids, T_effect_Vmax): - """Rate of protein synthesis (�mol` N proteins h-1 g-1 MS). + """Rate of protein synthesis (µmol` N proteins h-1 g-1 MS). Michaelis-Menten function of amino acids. - :param float amino_acids: Amount of amino acids (�mol` N) + :param float amino_acids: Amount of amino acids (µmol` N) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Protein synthesis (�mol` N h-1 g-1 mstruct) + :return: Protein synthesis (µmol` N h-1 g-1 mstruct) :rtype: float """ calculate_S_proteins = (((max(0., amino_acids) / (self.mstruct * self.__class__.PARAMETERS.ALPHA)) * self.__class__.PARAMETERS.VMAX_SPROTEINS) / @@ -1696,14 +1696,14 @@ def calculate_S_proteins(self, amino_acids, T_effect_Vmax): return calculate_S_proteins def calculate_D_Proteins(self, proteins, cytokinins, T_effect_Vmax): - """Rate of protein degradation (�mol` N proteins s-1 g-1 MS h-1). + """Rate of protein degradation (µmol` N proteins s-1 g-1 MS h-1). First order kinetic regulated by cytokinins concentration. - :param float proteins: Amount of proteins (�mol` N) + :param float proteins: Amount of proteins (µmol` N) :param float cytokinins: Amount of cytokinins (AU) :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of protein degradation (�mol` N g-1 mstruct) + :return: Rate of protein degradation (µmol` N g-1 mstruct) :rtype: float """ conc_proteins = proteins / (self.mstruct * self.__class__.PARAMETERS.ALPHA) @@ -1716,43 +1716,43 @@ def calculate_D_Proteins(self, proteins, cytokinins, T_effect_Vmax): parameters.SECOND_TO_HOUR_RATE_CONVERSION * regul_cytokinins * T_effect_Vmax) def calculate_Loading_Amino_Acids(self, amino_acids, amino_acids_phloem, mstruct_axis, T_effect_conductivity): - """Rate of amino acids loading to phloem (�mol` N amino acids h-1). + """Rate of amino acids loading to phloem (µmol` N amino acids h-1). Transport-resistance model. - :param float amino_acids: Amount of amino acids in the element (�mol` N) - :param float amino_acids_phloem: Amount of amino acids in the phloem (�mol` N) + :param float amino_acids: Amount of amino acids in the element (µmol` N) + :param float amino_acids_phloem: Amount of amino acids in the phloem (µmol` N) :param float mstruct_axis: Structural dry mass of the axis (g) - :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20�C (AU) + :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20°C (AU) - :return: Amino acids loading (�mol` N h-1) + :return: Amino acids loading (µmol` N h-1) :rtype: float """ Conc_Amino_Acids_element = amino_acids / (self.mstruct * self.__class__.PARAMETERS.ALPHA) Conc_Amino_Acids_phloem = amino_acids_phloem / (mstruct_axis * parameters.AXIS_PARAMETERS.ALPHA) - #: Driving compartment (�mol` N g-1 mstruct) + #: Driving compartment (µmol` N g-1 mstruct) driving_amino_acids_compartment = max(Conc_Amino_Acids_element, Conc_Amino_Acids_phloem) - #: Gradient of amino acids between the element and the phloem (�mol` N g-1 mstruct) + #: Gradient of amino acids between the element and the phloem (µmol` N g-1 mstruct) diff_amino_acids = Conc_Amino_Acids_element - Conc_Amino_Acids_phloem - #: Conductance depending on mstruct (g2 �mol`-1 s-1) + #: Conductance depending on mstruct (g2 µmol`-1 s-1) conductance = self.__class__.PARAMETERS.SIGMA_AMINO_ACIDS * self.__class__.PARAMETERS.BETA * self.mstruct ** (2 / 3) * T_effect_conductivity return driving_amino_acids_compartment * diff_amino_acids * conductance * parameters.SECOND_TO_HOUR_RATE_CONVERSION def calculate_Export_Amino_Acids(self, amino_acids, amino_acids_hiddenzone, mstruct_hiddenzone, T_effect_conductivity): - """Rate of amino acids exportation to hidden zone (�mol` N amino acids h-1). + """Rate of amino acids exportation to hidden zone (µmol` N amino acids h-1). Transport-resistance model. - :param float amino_acids: Amount of amino acids in the element (�mol` N) - :param float amino_acids_hiddenzone: Amino acids amount in the hidden zone (�mol` N) + :param float amino_acids: Amount of amino acids in the element (µmol` N) + :param float amino_acids_hiddenzone: Amino acids amount in the hidden zone (µmol` N) :param float mstruct_hiddenzone: mstruct of the hidden zone (g) - :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20�C (AU) + :param float T_effect_conductivity: Effect of the temperature on the conductivity rate at 20°C (AU) - :return: Rate of Amino acids export (�mol` N h-1) + :return: Rate of Amino acids export (µmol` N h-1) :rtype: float """ Conc_Amino_Acids_element = amino_acids / (self.mstruct * self.__class__.PARAMETERS.ALPHA) Conc_Amino_Acids_hiddenzone = amino_acids_hiddenzone / mstruct_hiddenzone - #: Gradient of amino acids between the element and the hidden zone (�mol` N g-1 mstruct) + #: Gradient of amino acids between the element and the hidden zone (µmol` N g-1 mstruct) diff_amino_acids = Conc_Amino_Acids_element - Conc_Amino_Acids_hiddenzone #: Conductance depending on mstruct conductance = HiddenZone.PARAMETERS.SIGMA * self.__class__.PARAMETERS.BETA * mstruct_hiddenzone ** (2 / 3) * T_effect_conductivity @@ -1795,12 +1795,12 @@ def calculate_D_cytokinins(self, cytokinins, T_effect_Vmax): def calculate_triosesP_derivative(self, Photosynthesis, S_Sucrose, S_Starch, S_Amino_Acids): """ delta triose phosphates of element. - :param float Photosynthesis: Total gross Photosynthesis (�mol` C) - :param float S_Sucrose: Sucrose synthesis (�mol` C g-1 mstruct) - :param float S_Starch: Starch synthesis (�mol` C g-1 mstruct) - :param float S_Amino_Acids: Amino acids synthesis (�mol` N g-1 mstruct) + :param float Photosynthesis: Total gross Photosynthesis (µmol` C) + :param float S_Sucrose: Sucrose synthesis (µmol` C g-1 mstruct) + :param float S_Starch: Starch synthesis (µmol` C g-1 mstruct) + :param float S_Amino_Acids: Amino acids synthesis (µmol` N g-1 mstruct) - :return: delta triose phosphates (�mol` C triose phosphates) + :return: delta triose phosphates (µmol` C triose phosphates) :rtype: float """ #: Contribution of triosesP to the synthesis of amino_acids @@ -1810,10 +1810,10 @@ def calculate_triosesP_derivative(self, Photosynthesis, S_Sucrose, S_Starch, S_A def calculate_starch_derivative(self, S_Starch, D_Starch): """delta starch of element. - :param float S_Starch: Starch synthesis (�mol` C g-1 mstruct) - :param float D_Starch: Starch degradation (�mol` C g-1 mstruct) + :param float S_Starch: Starch synthesis (µmol` C g-1 mstruct) + :param float D_Starch: Starch degradation (µmol` C g-1 mstruct) - :return: delta starch (�mol` C starch) + :return: delta starch (µmol` C starch) :rtype: float """ return (S_Starch - D_Starch) * (self.mstruct * self.__class__.PARAMETERS.ALPHA) @@ -1821,14 +1821,14 @@ def calculate_starch_derivative(self, S_Starch, D_Starch): def calculate_sucrose_derivative(self, S_Sucrose, D_Starch, Loading_Sucrose, S_Fructan, D_Fructan, sum_respi): """delta sucrose of element. - :param float S_Sucrose: Sucrose synthesis (�mol` C g-1 mstruct) - :param float D_Starch: Starch degradation (�mol` C g-1 mstruct) - :param float Loading_Sucrose: Sucrose loading (�mol` C) - :param float S_Fructan: Fructan synthesis (�mol` C g-1 mstruct) - :param float D_Fructan: Fructan degradation (�mol` C g-1 mstruct) - :param float sum_respi: Sum of respirations for the element i.e. related to C loading to phloem, amino acids synthesis and residual (�mol` C) + :param float S_Sucrose: Sucrose synthesis (µmol` C g-1 mstruct) + :param float D_Starch: Starch degradation (µmol` C g-1 mstruct) + :param float Loading_Sucrose: Sucrose loading (µmol` C) + :param float S_Fructan: Fructan synthesis (µmol` C g-1 mstruct) + :param float D_Fructan: Fructan degradation (µmol` C g-1 mstruct) + :param float sum_respi: Sum of respirations for the element i.e. related to C loading to phloem, amino acids synthesis and residual (µmol` C) - :return: delta sucrose (�mol` C sucrose) + :return: delta sucrose (µmol` C sucrose) :rtype: float """ return (S_Sucrose + D_Starch + D_Fructan - S_Fructan) * self.mstruct - sum_respi - Loading_Sucrose @@ -1836,10 +1836,10 @@ def calculate_sucrose_derivative(self, S_Sucrose, D_Starch, Loading_Sucrose, S_F def calculate_fructan_derivative(self, S_Fructan, D_Fructan): """delta fructan of element. - :param float S_Fructan: Fructan synthesis (�mol` C g-1 mstruct) - :param float D_Fructan: Fructan degradation (�mol` C g-1 mstruct) + :param float S_Fructan: Fructan synthesis (µmol` C g-1 mstruct) + :param float D_Fructan: Fructan degradation (µmol` C g-1 mstruct) - :return: delta fructan (�mol` C fructan) + :return: delta fructan (µmol` C fructan) :rtype: float """ return (S_Fructan - D_Fructan) * (self.mstruct * self.__class__.PARAMETERS.ALPHA) @@ -1847,10 +1847,10 @@ def calculate_fructan_derivative(self, S_Fructan, D_Fructan): def calculate_nitrates_derivative(self, Nitrates_import, S_Amino_Acids): """delta nitrates of element. - :param float Nitrates_import: Nitrate import from roots (�mol` N) - :param float S_Amino_Acids: Amino acids synthesis (�mol` N g-1 mstruct) + :param float Nitrates_import: Nitrate import from roots (µmol` N) + :param float S_Amino_Acids: Amino acids synthesis (µmol` N g-1 mstruct) - :return: delta nitrates (�mol` N nitrates) + :return: delta nitrates (µmol` N nitrates) :rtype: float """ nitrate_reduction_AA = S_Amino_Acids #: Contribution of nitrates to the synthesis of amino_acids @@ -1859,13 +1859,13 @@ def calculate_nitrates_derivative(self, Nitrates_import, S_Amino_Acids): def calculate_amino_acids_derivative(self, Amino_Acids_import, S_Amino_Acids, S_Proteins, D_Proteins, Loading_Amino_Acids): """delta amino acids of element. - :param float Amino_Acids_import: Amino acids import from roots (�mol` N) - :param float S_Amino_Acids: Amino acids synthesis (�mol` N g-1 mstruct) - :param float S_Proteins: Protein synthesis (�mol` N g-1 mstruct) - :param float D_Proteins: Protein degradation (�mol` N g-1 mstruct) - :param float Loading_Amino_Acids: Amino acids loading (�mol` N) + :param float Amino_Acids_import: Amino acids import from roots (µmol` N) + :param float S_Amino_Acids: Amino acids synthesis (µmol` N g-1 mstruct) + :param float S_Proteins: Protein synthesis (µmol` N g-1 mstruct) + :param float D_Proteins: Protein degradation (µmol` N g-1 mstruct) + :param float Loading_Amino_Acids: Amino acids loading (µmol` N) - :return: delta amino acids (�mol` N amino acids) + :return: delta amino acids (µmol` N amino acids) :rtype: float """ return Amino_Acids_import - Loading_Amino_Acids + (S_Amino_Acids + D_Proteins - S_Proteins) * (self.mstruct * self.__class__.PARAMETERS.ALPHA) @@ -1873,10 +1873,10 @@ def calculate_amino_acids_derivative(self, Amino_Acids_import, S_Amino_Acids, S_ def calculate_proteins_derivative(self, S_Proteins, D_Proteins): """delta proteins of element. - :param float S_Proteins: Protein synthesis (�mol` N g-1 mstruct) - :param float D_Proteins: Protein degradation (�mol` N g-1 mstruct) + :param float S_Proteins: Protein synthesis (µmol` N g-1 mstruct) + :param float D_Proteins: Protein degradation (µmol` N g-1 mstruct) - :return: delta proteins (�mol` N proteins) + :return: delta proteins (µmol` N proteins) :rtype: float """ return (S_Proteins - D_Proteins) * (self.mstruct * self.__class__.PARAMETERS.ALPHA) @@ -1948,8 +1948,8 @@ class Soil(object): def __init__(self, volume=None, nitrates=None, Tsoil=12, SRWC=100): """ :param float volume: volume of soil explored by roots (m3) - :param float nitrates: �mol` N nitrates - :param float Tsoil: soil temperature (�C) + :param float nitrates: µmol` N nitrates + :param float Tsoil: soil temperature (°C) :param float SRWC: soil relative water content (%) """ @@ -1963,15 +1963,15 @@ def __init__(self, volume=None, nitrates=None, Tsoil=12, SRWC=100): self.nitrates = nitrates # intermediate variables - self.Conc_Nitrates_Soil = None #: soil nitrate concentration Unloading (�mol` N m-3 soil) - self.mineralisation = None #: mineralisation on organic N into nitrates in soil (�mol`) + self.Conc_Nitrates_Soil = None #: soil nitrate concentration Unloading (µmol` N m-3 soil) + self.mineralisation = None #: mineralisation on organic N into nitrates in soil (µmol`) @staticmethod def calculate_temperature_effect_on_Vmax(Tsoil): """Effect of the temperature on maximal enzyme activity - Should multiply the rate at 20�C + Should multiply the rate at 20°C - :param float Tsoil: Soil temperature (�C) + :param float Tsoil: Soil temperature (°C) :return: Correction to apply to enzyme activity :rtype: float @@ -1992,9 +1992,9 @@ def calculate_temperature_effect_on_Vmax(Tsoil): @staticmethod def calculate_temperature_effect_on_conductivity(Tsoil): """Effect of the temperature on phloem translocation conductivity (Farrar 1988) - Should multiply the rate at 20�C + Should multiply the rate at 20°C - :param float Tsoil: Soil temperature (�C) + :param float Tsoil: Soil temperature (°C) :return: Correction to apply to conductivity coefficients. :rtype: float @@ -2009,9 +2009,9 @@ def calculate_temperature_effect_on_conductivity(Tsoil): def calculate_Conc_Nitrates(self, nitrates): """Nitrate concentration in soil. - :param float nitrates: Amount of nitrates (�mol` N) + :param float nitrates: Amount of nitrates (µmol` N) - :return: Nitrate concentration (�mol` nitrates m-3) + :return: Nitrate concentration (µmol` nitrates m-3) :rtype: float """ return max(0, (nitrates / self.volume)) @@ -2023,7 +2023,7 @@ def calculate_mineralisation(T_effect_Vmax): :param float T_effect_Vmax: Correction to apply to enzyme activity - :return: Rate of Nitrate mineralisation (�mol` h-1) + :return: Rate of Nitrate mineralisation (µmol` h-1) :rtype: float """ return parameters.SOIL_PARAMETERS.MINERALISATION_RATE * parameters.SECOND_TO_HOUR_RATE_CONVERSION * T_effect_Vmax @@ -2034,12 +2034,12 @@ def calculate_mineralisation(T_effect_Vmax): def calculate_nitrates_derivative(mineralisation, soil_contributors, culm_density, constant_Conc_Nitrates): """delta soil nitrates. - :param float mineralisation: N mineralisation in soil (�mol` m-2 N nitrates) - :param (float, int) soil_contributors: A tuple with (Nitrate uptake per axis (�mol` N nitrates), the plant id) + :param float mineralisation: N mineralisation in soil (µmol` m-2 N nitrates) + :param (float, int) soil_contributors: A tuple with (Nitrate uptake per axis (µmol` N nitrates), the plant id) :param dict [plant_id, culm_density] culm_density: A dictionary of culm density (culm_density = {plant_id: culm_density, ...}) :param bool constant_Conc_Nitrates: If True, the model run with a constant soil nitrate concentration. - :return: delta nitrates (�mol` N nitrates) + :return: delta nitrates (µmol` N nitrates) :rtype: float """ delta_Nitrates = 0 diff --git a/src/openalea/cnwgrass/cnmetabolism/simulation.py b/src/openalea/cnwgrass/cnmetabolism/simulation.py index 3567cc2..734b740 100644 --- a/src/openalea/cnwgrass/cnmetabolism/simulation.py +++ b/src/openalea/cnwgrass/cnmetabolism/simulation.py @@ -1402,7 +1402,11 @@ def _calculate_shoot_derivatives(self, t, y): for phytomer in shoot_axis.phytomers: # Hidden zone hiddenzone = phytomer.hiddenzone - if phytomer.hiddenzone is not None: + #: mstruct > 0 mirrors the exact same gate _initialize_indices_and_sparcity uses (and the + #: element loop just below already uses) to decide whether to assign _i_* indices at all -- + #: a brand-new hiddenzone (mstruct still 0 right at creation) never gets indexed there, so + #: reading y[hiddenzone._i_sucrose] here for one would raise AttributeError. + if phytomer.hiddenzone is not None and hiddenzone.mstruct > 0: hiddenzone.sucrose = y[hiddenzone._i_sucrose] hiddenzone.fructan = y[hiddenzone._i_fructan] hiddenzone.amino_acids = y[hiddenzone._i_amino_acids] @@ -1496,7 +1500,9 @@ def _calculate_shoot_derivatives(self, t, y): y_derivatives[i_proteins] = proteins_derivative y_derivatives[i_cytokinins] = cytokinins_derivative - if phytomer.hiddenzone is not None: + #: same mstruct > 0 gate as above -- a not-yet-indexed hiddenzone has no _i_* attributes to + #: write y_derivatives into further down this block. + if phytomer.hiddenzone is not None and hiddenzone.mstruct > 0: # Unloading of sucrose from phloem hiddenzone.Unloading_Sucrose = hiddenzone.calculate_Unloading_Sucrose(hiddenzone.sucrose, axis.phloem.sucrose, plant_shoot_mstruct, shoot_axis.T_effect_conductivity) diff --git a/src/openalea/cnwgrass/gasexchange/simulation.py b/src/openalea/cnwgrass/gasexchange/simulation.py index fa80a16..4c070f7 100644 --- a/src/openalea/cnwgrass/gasexchange/simulation.py +++ b/src/openalea/cnwgrass/gasexchange/simulation.py @@ -104,7 +104,7 @@ def run(self, Ta, ambient_CO2, RH, Ur): the results in :attr:`outputs`. :param float Ta: air temperature at t (degree Celsius) - :param float ambient_CO2: air CO2 at t (�mol mol-1) + :param float ambient_CO2: air CO2 at t (µmol mol-1) :param float RH: relative humidity at t (decimal fraction) :param float Ur: wind speed at the top of the canopy at t (m s-1) """ @@ -123,7 +123,7 @@ def run(self, Ta, ambient_CO2, RH, Ur): Ts = self.inputs['axes'][axis_id]['SAM_temperature'] else: - Ts = Ta # Initial value of Ts (�C) + Ts = Ta # Initial value of Ts (°C) #: Ci is stored as exactly 0 (see the surfacic_nitrogen == 0 branch below) whenever the element #: had no photosynthetic nitrogen at the previous step -- treat that the same as "no previous #: value" (like None/NaN) rather than feeding Ci=0 into calculate_photosynthesis, which divides @@ -131,7 +131,7 @@ def run(self, Ta, ambient_CO2, RH, Ur): if element_inputs['Ci'] is None or np.isnan(element_inputs['Ci']) or element_inputs['Ci'] <= 0: Ci = ambient_CO2 else: - Ci = element_inputs['Ci'] #: previous organ internal CO2 concentration (�mol mol-1) todo Ci = parameters.Ci_init_ratio * ambient_CO2 see with Victoria if we keep this + Ci = element_inputs['Ci'] #: previous organ internal CO2 concentration (µmol mol-1) todo Ci = parameters.Ci_init_ratio * ambient_CO2 see with Victoria if we keep this height_canopy = self.inputs['axes'][axis_id]['height_canopy'] water_potential = element_inputs.get('water_potential', None) diff --git a/src/openalea/cnwgrass/growth/simulation.py b/src/openalea/cnwgrass/growth/simulation.py index 720af0b..f514aff 100644 --- a/src/openalea/cnwgrass/growth/simulation.py +++ b/src/openalea/cnwgrass/growth/simulation.py @@ -255,13 +255,13 @@ def run(self, postflowering_stages=False): curr_hiddenzone_outputs['AA_consumption_mstruct'] = self.model.calculate_s_Nstruct_amino_acids((delta_leaf_enclosed_Nstruct + delta_internode_enclosed_Nstruct), delta_lamina_Nstruct, delta_sheath_Nstruct, - delta_internode_Nstruct) #: Consumption of amino acids due to mstruct growth (�mol N) + delta_internode_Nstruct) #: Consumption of amino acids due to mstruct growth (µmol N) curr_hiddenzone_outputs['sucrose_consumption_mstruct'] = self.model.calculate_s_mstruct_sucrose((delta_leaf_enclosed_mstruct + delta_internode_enclosed_mstruct), delta_lamina_mstruct, delta_sheath_mstruct, curr_hiddenzone_outputs[ - 'AA_consumption_mstruct']) #: Consumption of sucrose due to mstruct growth (�mol C) - curr_hiddenzone_outputs['Respi_growth'] = RespirationModel.R_growth(curr_hiddenzone_outputs['sucrose_consumption_mstruct']) #: Respiration growth (�mol C) + 'AA_consumption_mstruct']) #: Consumption of sucrose due to mstruct growth (µmol C) + curr_hiddenzone_outputs['Respi_growth'] = RespirationModel.R_growth(curr_hiddenzone_outputs['sucrose_consumption_mstruct']) #: Respiration growth (µmol C) # -- Update of hiddenzone outputs curr_hiddenzone_outputs['leaf_enclosed_mstruct'] += delta_leaf_enclosed_mstruct diff --git a/src/openalea/cnwgrass/hydraulics/model.py b/src/openalea/cnwgrass/hydraulics/model.py index a9ee423..08382a9 100644 --- a/src/openalea/cnwgrass/hydraulics/model.py +++ b/src/openalea/cnwgrass/hydraulics/model.py @@ -164,7 +164,7 @@ def __init__(self, index=None, lamina=None, internode=None, sheath=None, hiddenz self.hiddenzone = hiddenzone #: the hidden zone if cohorts is None: cohorts = [] - self.cohorts = cohorts #: list of cohort values - Hack to treat tillering cases : TEMPORARY. Devrait �tre port� � l'�chelle de la plante uniquement mais je ne vois pas comment faire mieux + self.cohorts = cohorts #: list of cohort values - Hack to treat tillering cases : TEMPORARY. Devrait être porté à l'échelle de la plante uniquement mais je ne vois pas comment faire mieux self.cohorts_replications = cohorts_replications #: dictionary of number of replications per cohort rank # integrative variables @@ -294,7 +294,7 @@ def __init__(self, label='hiddenzone', fructan=INIT_COMPARTMENTS.fructan, leaf_e if cohorts is None: cohorts = [] - self.cohorts = cohorts #: list of cohort values - Hack to treat tillering cases : TEMPORARY. Devrait �tre port� � l'�chelle de la plante uniquement mais je ne vois pas comment faire mieux + self.cohorts = cohorts #: list of cohort values - Hack to treat tillering cases : TEMPORARY. Devrait être porté à l'échelle de la plante uniquement mais je ne vois pas comment faire mieux self.cohorts_replications = cohorts_replications #: dictionary of number of replications per cohort rank self.index = index self.label = label @@ -304,12 +304,12 @@ def __init__(self, label='hiddenzone', fructan=INIT_COMPARTMENTS.fructan, leaf_e self.amino_acids = amino_acids #: :math:`:math:`\\mu mol N`` self.proteins = proteins #: :math:`:math:`\\mu mol N`` self.sucrose = sucrose #: :math:`:math:`\\mu mol C`` - self.leaf_pseudo_age = leaf_pseudo_age #: �Cd + self.leaf_pseudo_age = leaf_pseudo_age #: °Cd self.leaf_L = leaf_L #: m self.leaf_is_growing = leaf_is_growing #: - self.mstruct = mstruct #: g self.leaf_enclosed_mstruct = leaf_enclosed_mstruct #: g - self.hiddenzone_age = hiddenzone_age #: �Cd + self.hiddenzone_age = hiddenzone_age #: °Cd self.length = min(leaf_L, leaf_pseudostem_length) #: m self.leaf_pseudostem_length = leaf_pseudostem_length #: m self.lamina_Lmax = lamina_Lmax #: m @@ -379,11 +379,11 @@ def calculate_volume(water_content): def calculate_osmotic_water_potential(fructan, sucrose, amino_acids, volume, temperature): """ Osmotic water potential of the organ calculated according to metabolites - :param float fructan: �mol C under the form of fructan - :param float sucrose: �mol C under the form of sucrose - :param float amino_acids: �mol N under the form of amino acids + :param float fructan: µmol C under the form of fructan + :param float sucrose: µmol C under the form of sucrose + :param float amino_acids: µmol N under the form of amino acids :param float volume: (g H2O) - :param float temperature: hidden zone temperature, approximated by SAM temperature (�C) + :param float temperature: hidden zone temperature, approximated by SAM temperature (°C) :return: Osmotic water potential (MPa) :rtype: float @@ -524,7 +524,7 @@ def calculate_extensibility_temperature(age, delta_teq, delta_t): From Coussement et al. (2018) With temperature effect on leaf_pseudo_age and on maximum extensibility. - :param float age: hidden zone age (�Cd) + :param float age: hidden zone age (°Cd) :param float delta_teq: temperature-compensated time (s) :param float delta_t: time step of the simulation (s) @@ -733,14 +733,14 @@ def __init__(self, label=None, is_growing=INIT_COMPARTMENTS.is_growing, temperat self.label = label #: the label of the element self.index = index - if cohorts is None: #: list of cohort values - Hack to treat tillering cases : TEMPORARY. Devrait �tre port� � l'�chelle de la plante uniquement mais je ne vois pas comment faire mieux + if cohorts is None: #: list of cohort values - Hack to treat tillering cases : TEMPORARY. Devrait être porté à l'échelle de la plante uniquement mais je ne vois pas comment faire mieux cohorts = [] - self.cohorts = cohorts #: list of cohort values - Hack to treat tillering cases : TEMPORARY. Devrait �tre port� � l'�chelle de la plante uniquement mais je ne vois pas comment faire mieux + self.cohorts = cohorts #: list of cohort values - Hack to treat tillering cases : TEMPORARY. Devrait être porté à l'échelle de la plante uniquement mais je ne vois pas comment faire mieux self.cohorts_replications = cohorts_replications #: dictionary of number of replications per cohort rank # state parameters self.is_growing = is_growing #: - - self.age = age #: �Cd + self.age = age #: °Cd self.Wmax = Wmax #: m self.amino_acids = amino_acids #: :math:`:math:`\\mu mol N`` self.green_area = green_area #: m2 @@ -748,8 +748,8 @@ def __init__(self, label=None, is_growing=INIT_COMPARTMENTS.is_growing, temperat self.proteins = proteins #: :math:`:math:`\\mu mol N`` self.sucrose = sucrose #: :math:`:math:`\\mu mol C`` self.fructan = fructan #: :math:`:math:`\\mu mol C`` - self.Ts = Ts #: �C - self.temperature = temperature #: �C + self.Ts = Ts #: °C + self.temperature = temperature #: °C self.Tr = Tr #: mmol H20 m-2 s-1 self.thickness = thickness #: m self.width = width #: m @@ -858,11 +858,11 @@ def calculate_volume(water_content): def calculate_osmotic_water_potential(sucrose, amino_acids, volume, temperature, fructan): """ Osmotic water potential of the hiddenzone calculated according to metabolites - :param float sucrose: �mol C under the form of sucrose - :param float amino_acids: �mol N under the form of amino acids + :param float sucrose: µmol C under the form of sucrose + :param float amino_acids: µmol N under the form of amino acids :param float volume: (m3) - :param float temperature: air temperature (�C) - :param float fructan: �mol C under the form of fructan + :param float temperature: air temperature (°C) + :param float fructan: µmol C under the form of fructan :return: Osmotic water potential (MPa) :rtype: float diff --git a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py index a0181b4..5b02ed5 100644 --- a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py +++ b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py @@ -142,8 +142,8 @@ def run(self, Tair, Tsoil, update_shared_df=None): Run the model and update the MTG and the dataframes shared between all models. :param update_shared_df: - :param float Tair: air temperature (�C) - :param float Tsoil: soil temperature (�C) + :param float Tair: air temperature (°C) + :param float Tsoil: soil temperature (°C) :param bool update_shared_df: if 'True', update the shared dataframes at this time step. """ @@ -410,8 +410,8 @@ def _initialize_model(self, Tair, Tsoil): """ Initialize the inputs of the model from the MTG shared between all models and the soils. - :param float Tair: air temperature (�C) - :param float Tsoil: soil temperature (�C) + :param float Tair: air temperature (°C) + :param float Tsoil: soil temperature (°C) """ # Convert number of replications per tiller into number of replications per cohort. diff --git a/src/openalea/cnwgrass/integration/gasexchange_facade.py b/src/openalea/cnwgrass/integration/gasexchange_facade.py index cc0a12c..8ad6cc7 100644 --- a/src/openalea/cnwgrass/integration/gasexchange_facade.py +++ b/src/openalea/cnwgrass/integration/gasexchange_facade.py @@ -84,7 +84,7 @@ def run(self, Ta, ambient_CO2, RH, Ur, update_shared_df=None): Run the model and update the MTG and the dataframes shared between all models. :param float Ta: air temperature at t (degree Celsius) - :param float ambient_CO2: air CO2 at t (�mol mol-1) + :param float ambient_CO2: air CO2 at t (µmol mol-1) :param float RH: relative humidity at t (decimal fraction) :param float Ur: wind speed at the top of the canopy at t (m s-1) :param bool update_shared_df: if 'True', update the shared dataframes at this time step. diff --git a/src/openalea/cnwgrass/integration/hydraulics_facade.py b/src/openalea/cnwgrass/integration/hydraulics_facade.py index 9f70682..1b31c24 100644 --- a/src/openalea/cnwgrass/integration/hydraulics_facade.py +++ b/src/openalea/cnwgrass/integration/hydraulics_facade.py @@ -320,12 +320,34 @@ def _build_axis_phytomers(self, mtg_axis_vid, hydraulics_axis): #: (see hydraulics/simulation.py) -- correct and self-consistent whether this hiddenzone is #: genuinely fresh (tiny leaf_L) or already partway grown. if 'width' in missing_initial_hiddenzone_properties and not self._is_unusable_mtg_value(hydraulics_hiddenzone.leaf_L): - print("width not default anymore") hydraulics_hiddenzone.width = hydraulics_hiddenzone.leaf_L * hydraulics_model.HiddenZone.PARAMETERS.WL_ratio if 'thickness' in missing_initial_hiddenzone_properties and not self._is_unusable_mtg_value(hydraulics_hiddenzone.leaf_L): - print("thickness not default anymore") hydraulics_hiddenzone.thickness = hydraulics_hiddenzone.leaf_L * hydraulics_model.HiddenZone.PARAMETERS.TL_ratio + #: water_content/turgor_water_potential fall back to the exact same kind of flat, + #: pristine-hiddenzone default as width/thickness above: HIDDEN_ZONE_INIT_COMPARTMENTS + #: computes water_content from the CLASS's own tiny default leaf_L/width/thickness, and + #: turgor_water_potential from a fixed generic SRWC/osmotic potential -- neither reflects + #: this hiddenzone's real accumulated solutes or its (now correctly leaf_L-scaled) volume. + #: Since turgor drives the elastic length-growth term directly + #: (delta_turgor_water_potential -> delta_hiddenzone_dimensions_elastic, see + #: hydraulics/simulation.py), starting it wrong reproduces the exact same multi-hundred-hour + #: spurious elongation transient even with width/thickness already fixed above. Recompute + #: both consistently from this hiddenzone's own real state, mirroring exactly what the + #: model's own leaf_pseudo_age == 0 branch does for a genuinely fresh hiddenzone. + if ('water_content' in missing_initial_hiddenzone_properties or 'turgor_water_potential' in missing_initial_hiddenzone_properties) \ + and not self._is_unusable_mtg_value(hydraulics_hiddenzone.leaf_L) \ + and not self._is_unusable_mtg_value(hydraulics_hiddenzone.leaf_pseudostem_length): + hz_length = hydraulics_hiddenzone.calculate_hiddenzone_length(hydraulics_hiddenzone.leaf_L, hydraulics_hiddenzone.leaf_pseudostem_length) + hz_volume = hz_length * hydraulics_hiddenzone.width * hydraulics_hiddenzone.thickness + if 'water_content' in missing_initial_hiddenzone_properties: + hydraulics_hiddenzone.water_content = hz_volume * hydraulics_model.parameters.RHO_WATER + if 'turgor_water_potential' in missing_initial_hiddenzone_properties: + osmotic_water_potential = hydraulics_hiddenzone.calculate_osmotic_water_potential( + hydraulics_hiddenzone.fructan, hydraulics_hiddenzone.sucrose, hydraulics_hiddenzone.amino_acids, + hz_volume, hydraulics_axis.SAM_temperature) + hydraulics_hiddenzone.turgor_water_potential = hydraulics_axis.xylem.water_potential - osmotic_water_potential + #: leaf_Wmax/lamina_Lmax default to None (not a usable finite value like water_content/width/ #: thickness/turgor_water_potential do) -- if either is still unusable at this point (missing #: from the MTG or exempted above), seed it the same way the model itself does the first time diff --git a/src/openalea/cnwgrass/integration/senescence_facade.py b/src/openalea/cnwgrass/integration/senescence_facade.py index ac2356b..e79060c 100644 --- a/src/openalea/cnwgrass/integration/senescence_facade.py +++ b/src/openalea/cnwgrass/integration/senescence_facade.py @@ -221,7 +221,7 @@ def _update_shared_MTG(self, senescence_roots_data_dict, senescence_axes_data_di self._shared_mtg.property(senescence_element_data_name)[mtg_element_vid] = senescence_element_data_value # Temporaire avant de trouver une solution pour : # 1) piloter la senescence des feuilles par green_area plutot que par senesced_length, - # 2) updater les organes � partir des �l�ments et non l'inverse. + # 2) updater les organes à partir des éléments et non l'inverse. if senescence_element_data_name == 'senesced_length_element' and mtg_element_label in ['LeafElement1', 'StemElement']: self._shared_mtg.property('senesced_length')[mtg_organ_vid] = np.nan_to_num(self._shared_mtg.property(senescence_element_data_name).get(mtg_element_vid, 0.)) diff --git a/src/openalea/cnwgrass/morphogenesis/model.py b/src/openalea/cnwgrass/morphogenesis/model.py index 3024696..852e58e 100644 --- a/src/openalea/cnwgrass/morphogenesis/model.py +++ b/src/openalea/cnwgrass/morphogenesis/model.py @@ -130,7 +130,7 @@ def calculate_cumulated_thermal_time(self, sum_TT, temperature, delta_teq): :rtype: float """ if temperature > 0: - return sum_TT + delta_teq * self.parameters.Temp_Tref / 24.0 / 3600 # Conversion to days at 12�C + return sum_TT + delta_teq * self.parameters.Temp_Tref / 24.0 / 3600 # Conversion to days at 12°C else: return sum_TT @@ -245,9 +245,9 @@ def calculate_deltaL_preE(self, sucrose, leaf_L, amino_acids, mstruct, delta_teq xylem_water_potential=None): """ Delta of leaf length over delta_t as a function of sucrose and amino acids, from initiation to the emergence of the previous leaf. - :param float sucrose: Amount of sucrose (�mol C) + :param float sucrose: Amount of sucrose (µmol C) :param float leaf_L: Total leaf length (m) - :param float amino_acids: Amount of amino acids (�mol N) + :param float amino_acids: Amount of amino acids (µmol N) :param float mstruct: Structural mass (g) :param float delta_teq: Temperature-compensated time = time duration at a reference temperature (s) :param int leaf_rank: leaf phytomer number @@ -319,8 +319,8 @@ def calculate_deltaL_postE(self, prev_leaf_pseudo_age, leaf_pseudo_age, prev_lea :param float leaf_pseudo_age: Pseudo age of the leaf since beginning of automate elongation (s) :param float prev_leaf_L: Leaf length at previous time step (m) :param float leaf_Lmax: Final leaf length (m) - :param float sucrose: Amount of sucrose (�mol C) - :param float amino_acids: Amount of amino acids (�mol N) + :param float sucrose: Amount of sucrose (µmol C) + :param float amino_acids: Amount of amino acids (µmol N) :param float mstruct: Structural mass (g) :param int leaf_rank: rank of the leaf :param bool optimal_growth_option: if True the function will calculate leaf elongation assuming optimal growth conditions (except if sucrose and amino acids are zero) @@ -481,13 +481,13 @@ def calculate_mean_conc_sucrose(prev_mean_conc_sucrose, time_prev_leaf2_emergenc """ Update the mean sucrose concentration of the hiddenzone since leaf n-2 emergence. Calculation starts at leaf n-2 emergence, the updated mean accounts for the [sucrose] of current time step weighted by a function of temperature. - :param float prev_mean_conc_sucrose: Mean sucrose concentration of the hiddenzone at the end of previous simulation time step (�mol C g-1). + :param float prev_mean_conc_sucrose: Mean sucrose concentration of the hiddenzone at the end of previous simulation time step (µmol C g-1). :param float time_prev_leaf2_emergence: Time elapsed since leaf n-2 emergence (s at Tref). :param float delta_teq: Duration of the current simulation time step (s at Tref). - :param float sucrose: Sucrose in the hidden zone (�mol C). + :param float sucrose: Sucrose in the hidden zone (µmol C). :param float mstruct: Mstruct of the hidden zone (g). - :return: Updated mean sucrose concentration of the hiddenzone since leaf n-2 emergence (�mol C g-1) + :return: Updated mean sucrose concentration of the hiddenzone since leaf n-2 emergence (µmol C g-1) :rtype: float """ conc_sucrose = sucrose / mstruct @@ -506,7 +506,7 @@ def calculate_leaf_Wmax(self, lamina_Lmax, leaf_rank, integral_conc_sucr, optima :param float lamina_Lmax: Maximal lamina length (m) :param int leaf_rank: leaf phytomer number - :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (�mol C g-1) + :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (µmol C g-1) :param bool optimal_growth_option: if True the function will calculate leaf Wmax assuming optimal growth conditions :return: Maximal leaf width (m) @@ -529,7 +529,7 @@ def calculate_SSLW(self, leaf_rank, integral_conc_sucr, optimal_growth_option=Fa """ Structural Specific Lamina Weight. :param int leaf_rank: leaf phytomer number - :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (�mol C g-1) + :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (µmol C g-1) :param bool optimal_growth_option: if True the function will calculate SSLW assuming optimal growth conditions @@ -550,7 +550,7 @@ def calculate_LSSW(self, leaf_rank, integral_conc_sucr, optimal_growth_option=Fa """ Lineic Structural Sheath Weight. :param int leaf_rank: leaf phytomer number - :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (�mol C g-1) + :param float integral_conc_sucr: mean sucrose concentration of the hiddenzone since leaf n-2 emergence (µmol C g-1) :param bool optimal_growth_option: if True the function will calculate LSLW assuming optimal growth conditions :return: Lineic Structural Sheath Weight (g m-1) @@ -692,9 +692,9 @@ def calculate_delta_internode_L_preL(self, phytomer_rank, sucrose, internode_L, """ delta of internode length over delta_t as a function of sucrose and amino acids, from initiation to the ligulation of the previous leaf. :param int phytomer_rank: phytomer rank - :param float sucrose: Amount of sucrose (�mol C) + :param float sucrose: Amount of sucrose (µmol C) :param float internode_L: Total internode length (m) - :param float amino_acids: Amount of amino acids (�mol N) + :param float amino_acids: Amount of amino acids (µmol N) :param float mstruct: Structural mass of the hidden zone(g) :param float delta_teq: Temperature-compensated time = time duration at a reference temperature (s) :param bool optimal_growth_option: if True the function will calculate delta of internode length assuming optimal growth conditions @@ -776,9 +776,9 @@ def calculate_delta_internode_L_postL(self, prev_internode_pseudo_age, internode :param float internode_pseudo_age: Pseudo age of the internode since beginning of automate elongation (s) :param float prev_internode_L: Internode length before elongation (m) :param float internode_Lmax_lig: Estimate of final internode length at previous leaf ligulation (m) - :param float sucrose: Amount of sucrose (�mol C) - :param float amino_acids: Amount of amino acids (�mol N) - :param float mstruct: Structural mass (�mol N) + :param float sucrose: Amount of sucrose (µmol C) + :param float amino_acids: Amount of amino acids (µmol N) + :param float mstruct: Structural mass (µmol N) :param bool optimal_growth_option: if True the function will calculate delta of internode length assuming optimal growth conditions :return: internode_L (m) @@ -936,9 +936,9 @@ def calculate_deltaL_preE(self, sucrose, leaf_L, amino_acids, mstruct, delta_teq xylem_water_potential=0.): """ Delta of leaf length over delta_t as a function of sucrose and amino acids, from initiation to the emergence of the previous leaf. - :param float sucrose: Amount of sucrose (�mol C) + :param float sucrose: Amount of sucrose (µmol C) :param float leaf_L: Total leaf length (m) - :param float amino_acids: Amount of amino acids (�mol N) + :param float amino_acids: Amount of amino acids (µmol N) :param float mstruct: Structural mass (g) :param float delta_teq: Temperature-compensated time = time duration at a reference temperature (s) :param int leaf_rank: leaf phytomer number diff --git a/src/openalea/cnwgrass/morphogenesis/parameters.py b/src/openalea/cnwgrass/morphogenesis/parameters.py index 27e43a4..9112baa 100644 --- a/src/openalea/cnwgrass/morphogenesis/parameters.py +++ b/src/openalea/cnwgrass/morphogenesis/parameters.py @@ -114,9 +114,12 @@ def __init__(self): # --- Internodes # ------------------------------------------------------------------------------------------------------------------- # Exponential elongation - self.RERmax_dict_IN = {1: 2.4E-06, 2: 2.4E-06, 3: 1.85E-06, 4: 3.E-08, 5: 3.7E-06, 6: 3.3E-06, 7: 3.E-06, - 8: 2.E-06, 9: 2.E-06, 10: 2.5E-06, 11: 2.5E-06, 12: 1.5E-06} #: s-1 at 12°C FIT jan 20 - # RERmax_dict_IN = {1: 2.4E-06, 2: 2.4E-06, 3: 2.4E-06, 4: 2.4E-06, 5: 2.6E-06, 6: 2.6E-06, 7: 2.3E-06, 8: 1.9E-06, 9: 1.91E-06, 10: 1.9E-06, 11: 1.76E-06, 12: 1.5E-06} #: s-1 at 12°C FIT jan 20 + # self.RERmax_dict_IN = {1: 2.4E-06, 2: 2.4E-06, 3: 1.85E-06, 4: 3.E-08, 5: 3.7E-06, 6: 3.3E-06, 7: 3.E-06, + # 8: 2.E-06, 9: 2.E-06, 10: 2.5E-06, 11: 2.5E-06, 12: 1.5E-06} #: s-1 at 12°C FIT jan 20 + + # Used by Gauthier et al. 2020, before above values are updated by the from-germination version + self.RERmax_dict_IN = {1: 2.4E-06, 2: 2.4E-06, 3: 2.4E-06, 4: 2.4E-06, 5: 2.6E-06, 6: 2.6E-06, 7: 2.3E-06, + 8: 1.9E-06, 9: 1.91E-06, 10: 1.9E-06, 11: 1.76E-06, 12: 1.5E-06} #: s-1 at 12°C FIT jan 20 # { 3 : 2.48E-06 ,4 : 2.48E-06 ,5 : 2.48E-06 , 6 : 2.48E-06 , 7 : 2.48E-06 , 8 : 2.48E-06 , 9 : 2.48E-06 , 10 : 1.9E-06 , 11 : 1.6E-06 }#: s-1 at 12°C # estimate from Ljutovac 2002 over the period until leaf ligulation i.e. wider than in the model. diff --git a/src/openalea/cnwgrass/morphogenesis/simulation.py b/src/openalea/cnwgrass/morphogenesis/simulation.py index 2e481c5..dca41d1 100644 --- a/src/openalea/cnwgrass/morphogenesis/simulation.py +++ b/src/openalea/cnwgrass/morphogenesis/simulation.py @@ -171,7 +171,7 @@ def run(self, Tair, Tsoil, Zsowing=0.025): # hiddenzone initiation for i in range(0, init_leaf): # Initialise hiddenzone - hiddenzone_id = axis_id + tuple([1 + i + curr_axis_outputs['nb_leaves'] - init_leaf]) # TODO: peut etre simplifi� tant que 'calculate_SAM_status' renvoie 1 erreur si init_leaf>1 + hiddenzone_id = axis_id + tuple([1 + i + curr_axis_outputs['nb_leaves'] - init_leaf]) # TODO: peut etre simplifié tant que 'calculate_SAM_status' renvoie 1 erreur si init_leaf>1 new_hiddenzone = self.hiddenzone_init.__dict__.copy() self.outputs['hiddenzone'][hiddenzone_id] = new_hiddenzone @@ -642,6 +642,24 @@ def run(self, Tair, Tsoil, Zsowing=0.025): self.outputs['elements'][hidden_internode_id] = new_internode self.outputs['elements'][hidden_internode_id]['length'] = curr_hiddenzone_outputs['internode_L'] + #: A tiller's phytomer 1 has no "previous leaf" on its own axis to ligulate (tillers have no + #: coleoptile/phytomer 0) -- the prev_leaf_ligulated check just below can therefore never + #: become True for it, leaving it permanently stuck in the unbounded pre-ligulation growth + #: regime (calculate_delta_internode_L_preL), which then cascades via ligule height / + #: pseudostem length into every later phytomer on the same axis. Real tiller basal internodes + #: are negligibly short anyway -- stop it here, every timestep it's still growing (not just at + #: the one moment it was first initiated -- a tiller whose phytomer-1 internode was already + #: growing before this guard existed must be caught too). Deliberately NOT setting + #: internode_is_remobilizing=True: growth/simulation.py treats that as "elongation is fully + #: over", draining the WHOLE hiddenzone's sucrose/amino_acids/fructan/proteins into the + #: internode and marking the hiddenzone itself is_over=True for deletion -- which would starve + #: and delete the leaf sharing this hiddenzone before it ever gets to grow. Just stop the + #: internode itself. + if axe_label != 'MS' and phytomer_id == 1 and curr_hiddenzone_outputs['internode_is_growing']: + curr_hiddenzone_outputs['internode_is_growing'] = False + if hidden_internode_id in self.outputs['elements']: + self.outputs['elements'][hidden_internode_id]['is_growing'] = False + if curr_hiddenzone_outputs['internode_is_growing']: #: Found previous lamina to know if the previous leaf is ligulated.