From 1f57e77c3b5f82f36f8414e9b013877c86c5acea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 6 Aug 2026 12:11:03 +0200 Subject: [PATCH 01/21] start grass-bridges merge --- .../cnwgrass/integration/component.py | 748 ++++++++++++++++++ src/openalea/cnwgrass/integration/runner.py | 3 + 2 files changed, 751 insertions(+) create mode 100644 src/openalea/cnwgrass/integration/component.py diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py new file mode 100644 index 0000000..f105ab1 --- /dev/null +++ b/src/openalea/cnwgrass/integration/component.py @@ -0,0 +1,748 @@ +# -*- coding: latin-1 -*- + +import os +import numpy as np +import pandas as pd +import time +import logging + +from dataclasses import dataclass, fields +from openalea.metafspm.component import Model, declare + + +from openalea.adel.adel_dynamic import AdelDyn +from openalea.adel.echap_leaf import echap_leaves +from openalea.cnwgrass.integration import caribu_facade +from openalea.cnwgrass.integration import cnmetabolism_facade +from openalea.cnwgrass.integration import morphogenesis_facade +from openalea.cnwgrass.integration import gasexchange_facade +from openalea.cnwgrass.integration import build_outputs +from openalea.cnwgrass.integration import growth_facade +from openalea.cnwgrass.integration import senescence_facade +from openalea.cnwgrass.integration import hydraulics_facade + + +logger_output = logging.getLogger("Simulation_Logger") + +debug = False + +@dataclass +class CNW_Grass(Model): + """ + TODO : Add description + """ + + # Inputs expected from bellowground models + Export_Nitrates: float = declare(default=0., unit="umol.h-1", unit_comment="of nitrate", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") + Export_Amino_Acids: float = declare(default=0., unit="umol.h-1", unit_comment="of N", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") + Unloading_Sucrose_phloem: float = declare(default=0.1, unit="umol.h-1", unit_comment="of C", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") + Unloading_Amino_Acids_phloem: float = declare(default=0.1, unit="umol.h-1", unit_comment="of N", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") + sucrose_phloem_outside_solve: float = declare(default=10., unit="µmol of C", unit_comment="amount in equivalent C", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_carbon", state_variable_type="extensive", edit_by="user") + amino_acids_phloem_outside_solve: float = declare(default=1., unit="µmol of N", unit_comment="amount in equivalent N", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") + cytokinins: float = declare(default=0., unit="AU", unit_comment="", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") + mstruct: float = declare(default=0., unit="g", unit_comment="", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_carbon", state_variable_type="extensive", edit_by="user") + + # State variables condidered as outputs to bellowground models + Total_Transpiration: float = declare(default=0., unit="mmol.s-1", unit_comment="of water", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + mstruct_axis: float = declare(default=0.05, unit="g", unit_comment="of axis structural mass", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + sucrose_phloem: float = declare(default=10., unit="µmol", unit_comment="of C", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + amino_acids_phloem: float = declare(default=1, unit="µmol", unit_comment="of N", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + Unloading_Sucrose: float = declare(default=30., unit="umol.g-1.h-1", unit_comment="of equivalent C mol? sucrose", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + Unloading_Amino_Acids: float = declare(default=1., unit="umol.g-1.h-1", unit_comment="of equivalent amino acids N", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + Unloading_Sucrose_shoot_organs: float = declare(default=30., unit="umol.h-1", unit_comment="of equivalent C mol? sucrose", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + Unloading_Amino_Acids_shoot_organs: float = declare(default=1., unit="umol.h-1", unit_comment="of equivalent amino acids N", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + Export_cytokinins: float = declare(default=0., unit="AU.h-1", unit_comment="of cytokinins", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") + adventitious_to_emerge: list = declare(default=None, unit="", unit_comment="", description="List of adventitous roots delays before emergence starting from current time step, length of list indicates the number to emerge", + min_value="", max_value="", value_comment="", references="", DOI="", + variable_type="state_variable", by="model_shoot", state_variable_type="descriptor", edit_by="user") + + # PARAMETERS + synchronize_adventitious_emergence: bool = declare(default=True, unit="", unit_comment="", description="boolean to choose option where root nodal emergence depend on shoot leaf emergence dynamic", + min_value="", max_value="", value_comment="", references="", DOI="", + variable_type="parameter", by="model_shoot", state_variable_type="descriptor", edit_by="user") + nodal_emergence_delay_since_leaf_emerged: float = declare(default=(180 / 20) * 24 * 3600, unit="s", unit_comment="equivalent at 20°C", description="Emergence delay for nodal primordium to emerge since leaf emerged on this node", + min_value="", max_value="", value_comment="", references="Klepper 1984", DOI="", + variable_type="parameter", by="model_shoot", state_variable_type="descriptor", edit_by="user") + + + def __init__(self, root_mtg, meteo, inputs_dataframes, + HIDDENZONES_INITIAL_STATE_FILENAME = 'hiddenzones_initial_state.csv', ELEMENTS_INITIAL_STATE_FILENAME = 'elements_initial_state.csv', + AXES_INITIAL_STATE_FILENAME = 'axes_initial_state.csv', update_parameters_all_models = None, HOUR_TO_SECOND_CONVERSION_FACTOR = 3600, + ORGANS_INITIAL_STATE_FILENAME = 'organs_initial_state.csv', SOILS_INITIAL_STATE_FILENAME = 'soils_initial_state.csv', INPUTS_DIRPATH='inputs', + PLANT_DENSITY=None, tillers_replications=None, stored_times = None, N_fertilizations=None, computing_light_interception=True, heterogeneous_canopy=True, show_3Dplant = False, option_static = False, + isolated_roots = False, cnwheat_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): + + # SELF STORAGE FOR LOOP PARAMETERS + self.meteo = meteo + + # time steps + self.time_step_in_hours = START_TIME + self.CARIBU_TIMESTEP = CARIBU_TIMESTEP + self.SENESCENCE_TIMESTEP = SENESCENCE_TIMESTEP + self.MORPHOGENESIS_TIMESTEP = MORPHOGENESIS_TIMESTEP + self.GROWTH_TIMESTEP = GROWTH_TIMESTEP + self.CNMETABOLISM_TIMESTEP = CNMETABOLISM_TIMESTEP + self.HYDRAULICS_TIMESTEP = HYDRAULICS_TIMESTEP + + # canopy parameters + self.PLANT_DENSITY = PLANT_DENSITY + self.N_fertilizations = N_fertilizations + + # plant parameters + self.tillers_replications = tillers_replications + + # logging and data structures + self.stored_times = stored_times + self.shared_elements_inputs_outputs_df = pd.DataFrame() + self.shared_elements_inputs_outputs_df = pd.DataFrame() + self.shared_hiddenzones_inputs_outputs_df = pd.DataFrame() + self.shared_organs_inputs_outputs_df = pd.DataFrame() + self.shared_soils_inputs_outputs_df = pd.DataFrame() + self.shared_axes_inputs_outputs_df = pd.DataFrame() + self.all_simulation_steps = [] + self.axes_all_data_list = [] + self.organs_all_data_list = [] + self.hiddenzones_all_data_list = [] + self.elements_all_data_list = [] + self.soils_all_data_list = [] + + + # boolean choices + self.show_3Dplant = show_3Dplant + self.heterogeneous_canopy = heterogeneous_canopy + self.computing_light_interception = computing_light_interception + self.option_static = option_static + + # -- ADEL and MTG CONFIGURATION -- + + # read adelwheat inputs at t0 + self.adel_wheat = AdelDyn(seed=1, scene_unit='m', leaves=echap_leaves(xy_model='Soissons_byleafclass')) + self.g = self.adel_wheat.load(directory=INPUTS_DIRPATH) + + # Section specific to coupling with Root-BRIDGES + self.shoot_props = self.g.properties() + + self.props = root_mtg.properties() + self.vertices = root_mtg.vertices(scale=root_mtg.max_scale()) + + self.link_self_to_mtg() + + # --------------------------------------------- + # ----- CONFIGURATION OF THE FACADES ------- + # --------------------------------------------- + + # -- ELONGWHEAT (created first because it is the only facade to add new metamers) -- + # Initial states + elongwheat_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME][ + elongwheat_facade.converter.HIDDENZONE_TOPOLOGY_COLUMNS + [i for i in elongwheat_facade.simulation.HIDDENZONE_INPUTS if i in + inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME].columns]].copy() + elongwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ + elongwheat_facade.converter.ELEMENT_TOPOLOGY_COLUMNS + [i for i in elongwheat_facade.simulation.ELEMENT_INPUTS if i in + inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() + elongwheat_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ + elongwheat_facade.converter.AXIS_TOPOLOGY_COLUMNS + [i for i in elongwheat_facade.simulation.AXIS_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + + phytoT_df = pd.read_csv(os.path.join(INPUTS_DIRPATH, 'phytoT.csv')) + + # Update parameters if specified + if update_parameters_all_models and 'elongwheat' in update_parameters_all_models: + update_parameters_elongwheat = update_parameters_all_models['elongwheat'] + else: + update_parameters_elongwheat = None + + # Facade initialisation + self.elongwheat_facade_ = elongwheat_facade.ElongWheatFacade(self.g, + MORPHOGENESIS_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, + elongwheat_axes_initial_state, + elongwheat_hiddenzones_initial_state, + elongwheat_elements_initial_state, + self.shared_axes_inputs_outputs_df, + self.shared_hiddenzones_inputs_outputs_df, + self.shared_elements_inputs_outputs_df, + self.adel_wheat, phytoT_df, + update_parameters_elongwheat, + update_shared_df=UPDATE_SHARED_DF) + + # -- CARIBU -- + if self.computing_light_interception: + self.caribu_facade_ = caribu_facade.CaribuFacade(self.g, + self.shared_elements_inputs_outputs_df, + self.adel_wheat, + update_shared_df=UPDATE_SHARED_DF) + + # -- SENESCWHEAT -- + # Initial states + senescwheat_roots_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].loc[inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME]['organ'] == 'roots'][ + senescwheat_facade.converter.ROOTS_TOPOLOGY_COLUMNS + + [i for i in senescwheat_facade.converter.SENESCWHEAT_ROOTS_INPUTS if i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() + + senescwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ + senescwheat_facade.converter.ELEMENTS_TOPOLOGY_COLUMNS + + [i for i in senescwheat_facade.converter.SENESCWHEAT_ELEMENTS_INPUTS if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() + + senescwheat_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ + senescwheat_facade.converter.AXES_TOPOLOGY_COLUMNS + + [i for i in senescwheat_facade.converter.SENESCWHEAT_AXES_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + + # Update parameters if specified + if update_parameters_all_models and 'senescwheat' in update_parameters_all_models: + update_parameters_senescwheat = update_parameters_all_models['senescwheat'] + else: + update_parameters_senescwheat = None + + # Facade initialisation + self.senescwheat_facade_ = senescwheat_facade.SenescWheatFacade(self.g, + SENESCENCE_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, + senescwheat_roots_initial_state, + senescwheat_axes_initial_state, + senescwheat_elements_initial_state, + self.shared_organs_inputs_outputs_df, + self.shared_axes_inputs_outputs_df, + self.shared_elements_inputs_outputs_df, + update_parameters_senescwheat, + update_shared_df=UPDATE_SHARED_DF, + cnwheat_roots=cnwheat_roots) + + # -- FARQUHARWHEAT -- + # Initial states + farquharwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ + farquharwheat_facade.converter.ELEMENT_TOPOLOGY_COLUMNS + + [i for i in farquharwheat_facade.converter.FARQUHARWHEAT_ELEMENTS_INPUTS if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() + + farquharwheat_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ + farquharwheat_facade.converter.AXIS_TOPOLOGY_COLUMNS + + [i for i in farquharwheat_facade.converter.FARQUHARWHEAT_AXES_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + + # Use the initial version of the photosynthesis sub-model (as in Barillot et al. 2016, and in Gauthier et al. 2020) + update_parameters_farquharwheat = {'SurfacicProteins': False, 'NSC_Retroinhibition': False} + + # Facade initialisation + self.farquharwheat_facade_ = farquharwheat_facade.FarquharWheatFacade(self.g, + farquharwheat_elements_initial_state, + farquharwheat_axes_initial_state, + self.shared_elements_inputs_outputs_df, + update_parameters_farquharwheat, + update_shared_df=UPDATE_SHARED_DF) + + # -- GROWTHWHEAT -- + # Initial states + growthwheat_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME][ + growthwheat_facade.converter.HIDDENZONE_TOPOLOGY_COLUMNS + + [i for i in growthwheat_facade.simulation.HIDDENZONE_INPUTS if i in inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME].columns]].copy() + + growthwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ + growthwheat_facade.converter.ELEMENT_TOPOLOGY_COLUMNS + + [i for i in growthwheat_facade.simulation.ELEMENT_INPUTS if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() + + growthwheat_root_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].loc[inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME]['organ'] == 'roots'][ + growthwheat_facade.converter.ROOT_TOPOLOGY_COLUMNS + + [i for i in growthwheat_facade.simulation.ROOT_INPUTS if i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() + + growthwheat_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ + growthwheat_facade.converter.AXIS_TOPOLOGY_COLUMNS + + [i for i in growthwheat_facade.simulation.AXIS_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + + # Update parameters if specified + if update_parameters_all_models and 'growthwheat' in update_parameters_all_models: + update_parameters_growthwheat = update_parameters_all_models['growthwheat'] + else: + update_parameters_growthwheat = None + + # Facade initialisation + self.growthwheat_facade_ = growthwheat_facade.GrowthWheatFacade(self.g, + GROWTH_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, + growthwheat_hiddenzones_initial_state, + growthwheat_elements_initial_state, + growthwheat_root_initial_state, + growthwheat_axes_initial_state, + self.shared_organs_inputs_outputs_df, + self.shared_hiddenzones_inputs_outputs_df, + self.shared_elements_inputs_outputs_df, + self.shared_axes_inputs_outputs_df, + update_parameters_growthwheat, + update_shared_df=UPDATE_SHARED_DF, + cnwheat_roots=cnwheat_roots) + + # -- CNWHEAT -- + # Initial states + cnwheat_organs_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME][ + [i for i in cnwheat_facade.cnwheat_converter.ORGANS_VARIABLES if i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() + + cnwheat_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME][ + [i for i in cnwheat_facade.cnwheat_converter.HIDDENZONE_VARIABLES if i in inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME].columns]].copy() + + cnwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ + [i for i in cnwheat_facade.cnwheat_converter.ELEMENTS_VARIABLES if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() + + cnwheat_soils_initial_state = inputs_dataframes[SOILS_INITIAL_STATE_FILENAME][ + [i for i in cnwheat_facade.cnwheat_converter.SOILS_VARIABLES if i in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns]].copy() + + # Update parameters if specified + if update_parameters_all_models and 'cnwheat' in update_parameters_all_models: + update_parameters_cnwheat = update_parameters_all_models['cnwheat'] + else: + update_parameters_cnwheat = {} + + # Force solver separation if a different root model has been chosen + if not cnwheat_roots: + isolated_roots = True + + # Facade initialisation + self.cnwheat_facade_ = cnwheat_facade.CNWheatFacade(self.g, + CNMETABOLISM_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, + PLANT_DENSITY, + update_parameters_cnwheat, + cnwheat_organs_initial_state, + cnwheat_hiddenzones_initial_state, + cnwheat_elements_initial_state, + cnwheat_soils_initial_state, + self.shared_axes_inputs_outputs_df, + self.shared_organs_inputs_outputs_df, + self.shared_hiddenzones_inputs_outputs_df, + self.shared_elements_inputs_outputs_df, + self.shared_soils_inputs_outputs_df, + update_shared_df=UPDATE_SHARED_DF, + isolated_roots=isolated_roots, + cnwheat_roots=cnwheat_roots) + + # Run cnwheat with constant nitrates concentration in the soil if specified + if N_fertilizations is not None and 'constant_Conc_Nitrates' in N_fertilizations.keys(): + self.cnwheat_facade_.soils[(1, 'MS')].constant_Conc_Nitrates = True # TODO: make (1, 'MS') more general + self.cnwheat_facade_.soils[(1, 'MS')].nitrates = N_fertilizations['constant_Conc_Nitrates'] * self.cnwheat_facade_.soils[(1, 'MS')].volume + + # -- FSPMWHEAT -- + # Facade initialisation + self.fspmwheat_facade_ = fspmwheat_facade.FSPMWheatFacade(self.g) + + # Update geometry + self.adel_wheat.update_geometry(self.g) + if show_3Dplant: + self.adel_wheat.plot(self.g) + + self.cn_wheat_root_props = self.g.get_vertex_property(2)["roots"] + + # TODO : Temporary + self.cn_wheat_root_props["Unloading_Sucrose"] = self.props["Unloading_Sucrose"][1] + self.cn_wheat_root_props["Unloading_Amino_Acids"] = self.props["Unloading_Amino_Acids"][1] + self.g.properties()["Total_Transpiration"][2] = self.props["Total_Transpiration"][1] + + self.g.get_vertex_property(2)['phloem']['sucrose'] = self.props["sucrose_phloem"][1] + self.g.get_vertex_property(2)['phloem']['amino_acids'] = self.props["amino_acids_phloem"][1] + + + if self.synchronize_adventitious_emergence: + # Specific initialization for already emerged leaves at the begining of the simulation + self.main_stem_vid = [v for v in self.g.vertices(scale=2) if "MS" in str(self.g.node(v).label)][0] + self.already_emerged_leaves = [] + hiddenzones = self.g.vertices(scale=3) + for v in hiddenzones: + n = self.g.node(v) + if hasattr(n, "hiddenzone"): + if n.hiddenzone is not None: + hz = n.hiddenzone + if hz["leaf_is_emerged"]: + self.already_emerged_leaves.append(v) + nodal_emergence_delays = [] + for vid in self.g.components_at_scale(self.main_stem_vid, scale=5): + n = self.g.node(vid) + if "Leaf" in n.label: + remaining_time_to_emergence = max(0, self.nodal_emergence_delay_since_leaf_emerged - (n.age / 20) * 24 * 3600) + nodal_emergence_delays += [remaining_time_to_emergence, + remaining_time_to_emergence] + # Third likely nodal on this node + if np.random.random() < 0.8: + nodal_emergence_delays.append(remaining_time_to_emergence + (100 / 20) * 24 * 3600 ) + # Fourth less likely nodal on this node + elif np.random.random() < 0.2: + nodal_emergence_delays.append(remaining_time_to_emergence + (100 / 20) * 24 * 3600 ) + + self.props["adventitious_to_emerge"].update({1: nodal_emergence_delays}) + + # TODO Temporary while an initialization bug for shared mtg is still present + self.g.get_vertex_property(2)['phloem']['Unloading_Sucrose_shoot_organs'] = 30. + self.g.get_vertex_property(2)['phloem']['Unloading_Amino_Acids_shoot_organs'] = 1. + + self.sync_shoot_outputs_with_root_mtg() + + + def sync_shoot_inputs_with_shoot_mtg(self): + for name in self.inputs: + if name == "Unloading_Sucrose_phloem": + self.cn_wheat_root_props["Unloading_Sucrose"] = self.props[name][1] / self.props['mstruct'][1] + + elif name == "Unloading_Amino_Acids_phloem": + self.cn_wheat_root_props["Unloading_Amino_Acids"] = self.props[name][1] / self.props['mstruct'][1] + else: + self.cn_wheat_root_props[name] = self.props[name][1] + + def sync_shoot_outputs_with_root_mtg(self): + # Link this specific data structure to self for variables exchange, only for outputs that will be read by other models here. + # Note : here eval is necessary to ensure intended lambda function definition + for name in self.state_variables: + if name == "Total_Transpiration": + self.props[name].update({1: self.g.get_vertex_property(2)[name]}) + + elif name == "mstruct_axis": + self.props[name].update({1: self.g.get_vertex_property(2)['mstruct']}) + + elif name == "amino_acids_phloem": + self.props[name].update({1: self.g.get_vertex_property(2)['phloem']['amino_acids']}) + + elif name == "sucrose_phloem": + self.props[name].update({1: self.g.get_vertex_property(2)['phloem']['sucrose']}) + + # Was added to Simulation.ORGANS_INTEGRATIVE_VARIABLES to be recognized + elif name == "Unloading_Sucrose_shoot_organs": + self.props[name].update({1: self.g.get_vertex_property(2)['phloem']['Unloading_Sucrose_shoot_organs']}) + + elif name == "Unloading_Amino_Acids_shoot_organs": + self.props[name].update({1: self.g.get_vertex_property(2)['phloem']['Unloading_Amino_Acids_shoot_organs']}) + + elif name == "adventitious_to_emerge" and self.synchronize_adventitious_emergence: + hiddenzones = self.g.vertices(scale=3) + main_stem_hz = self.g.components_at_scale(self.main_stem_vid, scale=3) + nodal_emergence_delays = [] # Several per time step are possible! + for v in hiddenzones: + n = self.g.node(v) + + if hasattr(n, "hiddenzone"): + if n.hiddenzone is not None: + hz = n.hiddenzone + if hz["leaf_is_emerged"] and v not in self.already_emerged_leaves: + nodal_emergence_delays += [self.nodal_emergence_delay_since_leaf_emerged, + self.nodal_emergence_delay_since_leaf_emerged] + if v in main_stem_hz: + # Third likely nodal on this node + if np.random.random() < 0.8: + nodal_emergence_delays.append(self.nodal_emergence_delay_since_leaf_emerged + (100 / 20) * 24 * 3600 ) + # Fourth less likely nodal on this node + elif np.random.random() < 0.2: + nodal_emergence_delays.append(self.nodal_emergence_delay_since_leaf_emerged + (100 / 20) * 24 * 3600 ) + + self.props[name].update({1: nodal_emergence_delays}) + self.already_emerged_leaves.append(v) + + else: + self.props[name].update({1: self.cn_wheat_root_props[name]}) + + + + + def __call__(self): + # SPECIFIC TO COUPLING, syncs the shoot mtg variables with bellowground models + self.pull_available_inputs() + self.sync_shoot_inputs_with_shoot_mtg() + + # run Caribu + PARi = self.meteo.loc[self.time_step_in_hours, ['PARi']].iloc[0] + DOY = self.meteo.loc[self.time_step_in_hours, ['DOY']].iloc[0] + hour = self.meteo.loc[self.time_step_in_hours, ['hour']].iloc[0] + + if self.computing_light_interception: + PARi_next_hours = self.meteo.loc[range(self.time_step_in_hours, self.time_step_in_hours + self.CARIBU_TIMESTEP), ['PARi']].sum().values[0] + + if (self.time_step_in_hours % self.CARIBU_TIMESTEP == 0) and (PARi_next_hours > 0): + run_caribu = True + else: + run_caribu = False + + self.caribu_facade_.run(run_caribu, energy=PARi, DOY=DOY, hourTU=hour, latitude=48.85, sun_sky_option='sky', heterogeneous_canopy=self.heterogeneous_canopy, plant_density=self.PLANT_DENSITY[1]) + + for t_senescwheat in range(self.time_step_in_hours, self.time_step_in_hours + self.SENESCENCE_TIMESTEP, self.SENESCENCE_TIMESTEP): + # run SenescWheat + t1 = time.time() + self.senescwheat_facade_.run() + # if (1, 'MS', 5, 'internode', 'HiddenElement') not in self.senescwheat_facade_._simulation.outputs["elements"].keys(): + # logger_output.info("Post SW, Internode not found") + hidelt = self.g.node(80) + if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): + logger_output.info(f"Post SW, Internode element has no proper length in MTG : {hidelt.length}") + hidorg = self.g.node(70) + if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): + logger_output.info(f"Post SW, Internode organ has no proper length in MTG : {hidorg.length}") + if debug: print("senescwheat took :", time.time() - t1) + + # Test for dead plant # TODO: adapt in case of multiple plants + if not self.shared_elements_inputs_outputs_df.empty and \ + np.nansum(self.shared_elements_inputs_outputs_df.loc[self.shared_elements_inputs_outputs_df['element'].isin(['StemElement', 'LeafElement1']), 'green_area']) == 0: + # append the inputs and outputs at current step to global lists + self.all_simulation_steps.append(t_senescwheat) + self.axes_all_data_list.append(self.shared_axes_inputs_outputs_df.copy()) + self.organs_all_data_list.append(self.shared_organs_inputs_outputs_df.copy()) + self.hiddenzones_all_data_list.append(self.shared_hiddenzones_inputs_outputs_df.copy()) + self.elements_all_data_list.append(self.shared_elements_inputs_outputs_df.copy()) + self.soils_all_data_list.append(self.shared_soils_inputs_outputs_df.copy()) + break + + # Run the rest of the model if the plant is alive + for t_farquharwheat in range(t_senescwheat, t_senescwheat + self.SENESCENCE_TIMESTEP, self.FARQUHARWHEAT_TIMESTEP): + # get the meteo of the current step + Ta, ambient_CO2, RH, Ur = self.meteo.loc[t_farquharwheat, ['air_temperature', 'ambient_CO2', 'humidity', 'Wind']] + + # run FarquharWheat + t1 = time.time() + self.farquharwheat_facade_.run(Ta, ambient_CO2, RH, Ur) + # if (1, 'MS', 5, 'internode', 'HiddenElement') not in self.farquharwheat_facade_._simulation.outputs.keys(): + # logger_output.info("Post FW, Internode not found") + hidelt = self.g.node(80) + if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): + logger_output.info(f"Post FW, Internode element has no proper length in MTG : {hidelt.length}") + hidorg = self.g.node(70) + if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): + logger_output.info(f"Post FW, Internode organ has no proper length in MTG : {hidorg.length}") + if debug: print("farquharwheat took :", time.time() - t1) + + for t_elongwheat in range(t_farquharwheat, t_farquharwheat + self.FARQUHARWHEAT_TIMESTEP, self.MORPHOGENESIS_TIMESTEP): + # run ElongWheat + Tair, Tsoil = self.meteo.loc[t_elongwheat, ['air_temperature', 'soil_temperature']] + t1 = time.time() + self.elongwheat_facade_.run(Tair, Tsoil, option_static=self.option_static) + # if (1, 'MS', 5, 'internode', 'HiddenElement') not in self.elongwheat_facade_._simulation.outputs["elements"].keys(): + # logger_output.info("Post EW, Internode not found") + hidelt = self.g.node(80) + if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): + logger_output.info(f"Post EW, Internode element has no proper length in MTG : {hidelt.length}") + hidorg = self.g.node(70) + if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): + logger_output.info(f"Post EW, Internode organ has no proper length in MTG : {hidorg.length}") + if debug: print("elongwheat took :", time.time() - t1) + + # Update geometry + self.adel_wheat.update_geometry(self.g) + if self.show_3Dplant: + self.adel_wheat.plot(self.g) + hidelt = self.g.node(80) + if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): + logger_output.info(f"Post AW, Internode element has no proper length in MTG : {hidelt.length}") + hidorg = self.g.node(70) + if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): + logger_output.info(f"Post AW, Internode organ has no proper length in MTG : {hidorg.length}") + + for t_growthwheat in range(t_elongwheat, t_elongwheat + self.MORPHOGENESIS_TIMESTEP, self.GROWTH_TIMESTEP): + # run GrowthWheat + t1 = time.time() + self.growthwheat_facade_.run() + # if (1, 'MS', 5, 'internode', 'HiddenElement') not in self.growthwheat_facade_._simulation.outputs["elements"].keys(): + # logger_output.info("Post GW, Internode not found") + hidelt = self.g.node(80) + if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): + logger_output.info(f"Post GW, Internode element has no proper length in MTG : {hidelt.length}") + hidorg = self.g.node(70) + if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): + logger_output.info(f"Post GW, Internode organ has no proper length in MTG : {hidorg.length}") + + for t_cnwheat in range(t_growthwheat, t_growthwheat + self.GROWTH_TIMESTEP, self.CNMETABOLISM_TIMESTEP): + #print('t cnwheat is {}'.format(t_cnwheat)) + + # N fertilization if any + if self.N_fertilizations is not None and len(self.N_fertilizations) > 0: + if t_cnwheat in self.N_fertilizations.keys(): + self.cnwheat_facade_.soils[(1, 'MS')].nitrates += self.N_fertilizations[t_cnwheat] + + if t_cnwheat > 0: + # run CNWheat + Tair = self.meteo.loc[t_elongwheat, 'air_temperature'] + Tsoil = self.meteo.loc[t_elongwheat, 'soil_temperature'] + t1 = time.time() + self.cnwheat_facade_.run(Tair, Tsoil, self.tillers_replications) + if debug: print("cnwheat took :", time.time() - t1) + hidelt = self.g.node(80) + if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): + logger_output.info(f"Post CNW, Internode element has no proper length in MTG : {hidelt.length}") + hidorg = self.g.node(70) + if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): + logger_output.info(f"Post CNW, Internode organ has no proper length in MTG : {hidorg.length}") + + # append outputs at current step to global lists + if (self.stored_times == 'all') or (t_cnwheat in self.stored_times): + axes_outputs, elements_outputs, hiddenzones_outputs, organs_outputs, soils_outputs = self.fspmwheat_facade_.build_outputs_df_from_MTG() + + self.all_simulation_steps.append(t_cnwheat) + self.axes_all_data_list.append(axes_outputs) + self.organs_all_data_list.append(organs_outputs) + self.hiddenzones_all_data_list.append(hiddenzones_outputs) + self.elements_all_data_list.append(elements_outputs) + self.soils_all_data_list.append(soils_outputs) + + self.time_step_in_hours += self.SENESCENCE_TIMESTEP + + self.sync_shoot_outputs_with_root_mtg() + + + +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}, + stored_times = None, option_static = False, show_3Dplant = False, run_from_outputs = False, heterogeneous_canopy = True, update_parameters_all_models = None, + isolated_roots = False, cnwheat_roots = True): + scenario = {} + + ### DIRS ### + + scenario["INPUTS_DIRPATH"] = INPUTS_DIRPATH + + # Save the outputs with a full scan of the MTG at each time step (or at selected time steps) + UPDATE_SHARED_DF = False + if stored_times is None: + stored_times = 'all' + if not (stored_times == 'all' or type(stored_times) == list): + print('stored_times should be either \'all\', a list or an empty list.') + raise + + scenario["stored_times"] = stored_times + + ### METEO PARAMETER ### + scenario["meteo"] = pd.read_csv(os.path.join(INPUTS_DIRPATH, METEO_FILENAME), index_col='t') + + AXES_INDEX_COLUMNS = ['t', 'plant', 'axis'] + ELEMENTS_INDEX_COLUMNS = ['t', 'plant', 'axis', 'metamer', 'organ', 'element'] + HIDDENZONES_INDEX_COLUMNS = ['t', 'plant', 'axis', 'metamer'] + ORGANS_INDEX_COLUMNS = ['t', 'plant', 'axis', 'organ'] + SOILS_INDEX_COLUMNS = ['t', 'plant', 'axis'] + + # Name of the CSV files which describes the initial state of the system + AXES_INITIAL_STATE_FILENAME = 'axes_initial_state.csv' + ORGANS_INITIAL_STATE_FILENAME = 'organs_initial_state.csv' + HIDDENZONES_INITIAL_STATE_FILENAME = 'hiddenzones_initial_state.csv' + ELEMENTS_INITIAL_STATE_FILENAME = 'elements_initial_state.csv' + SOILS_INITIAL_STATE_FILENAME = 'soils_initial_state.csv' + + # Name of the CSV files which will contain the outputs of the model + AXES_OUTPUTS_FILENAME = 'axes_outputs.csv' + ORGANS_OUTPUTS_FILENAME = 'organs_outputs.csv' + HIDDENZONES_OUTPUTS_FILENAME = 'hiddenzones_outputs.csv' + ELEMENTS_OUTPUTS_FILENAME = 'elements_outputs.csv' + SOILS_OUTPUTS_FILENAME = 'soils_outputs.csv' + + ### INPUT DATAFRAMES PARAMETER ### + # Read the inputs from CSV files and create inputs dataframes + inputs_dataframes = {} + if run_from_outputs: + previous_axes_outputs_dataframe = pd.read_csv(os.path.join(OUTPUTS_DIRPATH, AXES_OUTPUTS_FILENAME)) + assert 't' in previous_axes_outputs_dataframe.columns + if forced_start_time > 0: + new_start_time = forced_start_time + 1 + else: + last_t_step = int(previous_axes_outputs_dataframe['t'].max()) + new_start_time = last_t_step + 1 + + previous_outputs_dataframes = {} + + for initial_state_filename, outputs_filename, index_columns in ((AXES_INITIAL_STATE_FILENAME, AXES_OUTPUTS_FILENAME, AXES_INDEX_COLUMNS), + (ORGANS_INITIAL_STATE_FILENAME, ORGANS_OUTPUTS_FILENAME, ORGANS_INDEX_COLUMNS), + (HIDDENZONES_INITIAL_STATE_FILENAME, HIDDENZONES_OUTPUTS_FILENAME, HIDDENZONES_INDEX_COLUMNS), + (ELEMENTS_INITIAL_STATE_FILENAME, ELEMENTS_OUTPUTS_FILENAME, ELEMENTS_INDEX_COLUMNS), + (SOILS_INITIAL_STATE_FILENAME, SOILS_OUTPUTS_FILENAME, SOILS_INDEX_COLUMNS)): + + previous_outputs_dataframe = pd.read_csv(os.path.join(OUTPUTS_DIRPATH, outputs_filename)) + # Convert NaN to None + previous_outputs_dataframes[outputs_filename] = previous_outputs_dataframe.where(previous_outputs_dataframe.notnull(), None) + + # assert 't' in previous_outputs_dataframes[outputs_filename].columns + if forced_start_time > 0: + previous_outputs_dataframes[outputs_filename] = previous_outputs_dataframes[outputs_filename][previous_outputs_dataframes[outputs_filename]['t'] <= forced_start_time] + + if initial_state_filename == ELEMENTS_INITIAL_STATE_FILENAME: + elements_previous_outputs = previous_outputs_dataframes[outputs_filename] + new_initial_state = elements_previous_outputs[~elements_previous_outputs.is_over.isnull()] + else: + new_initial_state = previous_outputs_dataframes[outputs_filename] + idx = new_initial_state.groupby([col for col in index_columns if col != 't'])['t'].transform(max) == new_initial_state['t'] + inputs_dataframes[initial_state_filename] = new_initial_state[idx].drop(['t'], axis=1) + + else: + new_start_time = -1 + for inputs_filename in (AXES_INITIAL_STATE_FILENAME, + ORGANS_INITIAL_STATE_FILENAME, + HIDDENZONES_INITIAL_STATE_FILENAME, + ELEMENTS_INITIAL_STATE_FILENAME, + SOILS_INITIAL_STATE_FILENAME): + inputs_dataframe = pd.read_csv(os.path.join(INPUTS_DIRPATH, inputs_filename)) + inputs_dataframes[inputs_filename] = inputs_dataframe.where(inputs_dataframe.notnull(), None) + + # Start time of the simulation + START_TIME = max(0, new_start_time) + scenario["START_TIME"] = START_TIME + + # Pass a unified time-step + + # TODO constrained to interger for now, and using float, so bellow hour time stepping breaks both for loops and input dataframe accessions + time_step_in_hours = int(time_step_in_seconds / 3600) + scenario["CARIBU_TIMESTEP"] = time_step_in_hours + scenario["FARQUHARWHEAT_TIMESTEP"] = time_step_in_hours + scenario["MORPHOGENESIS_TIMESTEP"] = time_step_in_hours + scenario["GROWTH_TIMESTEP"] = time_step_in_hours + scenario["CNMETABOLISM_TIMESTEP"] = time_step_in_hours + scenario["SENESCENCE_TIMESTEP"] = time_step_in_hours + + scenario["inputs_dataframes"] = inputs_dataframes + + ### OPTIONS ### + scenario["PLANT_DENSITY"] = PLANT_DENSITY + scenario["option_static"] = option_static + scenario["show_3Dplant"] = show_3Dplant + scenario["tillers_replications"] = tillers_replications + scenario["heterogeneous_canopy"] = heterogeneous_canopy + scenario["N_fertilizations"] = N_fertilizations + scenario["update_parameters_all_models"] = update_parameters_all_models + scenario["isolated_roots"] = isolated_roots + scenario["cnwheat_roots"] = cnwheat_roots + + + return scenario + + +def save_df_to_csv(df, outputs_filepath, precision): + """ + Write outputs of the model + + :param pandas.DataFrame df: the current output dataframe + :param str outputs_filepath: path of the output file + :param int precision: number of digit + + """ + + try: + df.to_csv(outputs_filepath, na_rep='NA', index=False, float_format='%.{}f'.format(precision)) + except IOError as err: + path, filename = os.path.split(outputs_filepath) + filename = os.path.splitext(filename)[0] + newfilename = 'ACTUAL_{}.csv'.format(filename) + newpath = os.path.join(path, newfilename) + df.to_csv(newpath, na_rep='NA', index=False, float_format='%.{}f'.format(precision)) + warnings.warn('[{}] {}'.format(err.errno, err.strerror)) + warnings.warn('File will be saved at {}'.format(newpath)) \ No newline at end of file diff --git a/src/openalea/cnwgrass/integration/runner.py b/src/openalea/cnwgrass/integration/runner.py index ca70a42..60328e4 100644 --- a/src/openalea/cnwgrass/integration/runner.py +++ b/src/openalea/cnwgrass/integration/runner.py @@ -151,9 +151,11 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin GROWTH_TIMESTEP = 1 CNMETABOLISM_TIMESTEP = 1 hydraulics_TIMESTEP = 1 + # Done # precision of floats used to write and format the output CSV files OUTPUTS_PRECISION = 8 + # TODO check csv writing # number of seconds in 1 hour HOUR_TO_SECOND_CONVERSION_FACTOR = 3600 @@ -222,6 +224,7 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin # Start time of the simulation START_TIME = max(0, new_start_time) + # Done # Name of the CSV files which contains the meteo data meteo = pd.read_csv(os.path.join(INPUTS_DIRPATH, METEO_FILENAME), index_col='t') From ea5f0de9e936ef87e02dcee6b868d683289daa85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 6 Aug 2026 14:30:30 +0200 Subject: [PATCH 02/21] component init in line with both versions --- .../cnwgrass/integration/component.py | 422 ++++++++++-------- 1 file changed, 228 insertions(+), 194 deletions(-) diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index f105ab1..3fd2c60 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -101,10 +101,11 @@ class CNW_Grass(Model): def __init__(self, root_mtg, meteo, inputs_dataframes, HIDDENZONES_INITIAL_STATE_FILENAME = 'hiddenzones_initial_state.csv', ELEMENTS_INITIAL_STATE_FILENAME = 'elements_initial_state.csv', - AXES_INITIAL_STATE_FILENAME = 'axes_initial_state.csv', update_parameters_all_models = None, HOUR_TO_SECOND_CONVERSION_FACTOR = 3600, + AXES_INITIAL_STATE_FILENAME = 'axes_initial_state.csv', update_parameters_all_models = None, step_callback=None, HOUR_TO_SECOND_CONVERSION_FACTOR = 3600, ORGANS_INITIAL_STATE_FILENAME = 'organs_initial_state.csv', SOILS_INITIAL_STATE_FILENAME = 'soils_initial_state.csv', INPUTS_DIRPATH='inputs', - PLANT_DENSITY=None, tillers_replications=None, stored_times = None, N_fertilizations=None, computing_light_interception=True, heterogeneous_canopy=True, show_3Dplant = False, option_static = False, - isolated_roots = False, cnwheat_roots=True, UPDATE_SHARED_DF=False, START_TIME = 0, + MANAGEMENT_FILENAME = 'management.csv', tillers_replications=None, stored_times = None, computing_light_interception=True, 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): # SELF STORAGE FOR LOOP PARAMETERS @@ -120,20 +121,27 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.HYDRAULICS_TIMESTEP = HYDRAULICS_TIMESTEP # canopy parameters - self.PLANT_DENSITY = PLANT_DENSITY - self.N_fertilizations = N_fertilizations + # Management data + management_df = pd.read_csv(os.path.join(INPUTS_DIRPATH, MANAGEMENT_FILENAME), header=0, index_col=0) + management_variables = {} + for var_name in management_df.index: + management_variables[var_name] = get_management_value(management_df, var_name) + self.plant_density = management_variables.get('plant_density', {1: 250}) + self.inter_row = management_variables.get('inter_row', 0.15) + self.Zsowing = management_variables.get('sowing_depth', 0.025) + self.N_fertilizations = management_variables.get('N_fertilizations', {}) + # TODO choose between scenario passing or management dependancy # plant parameters self.tillers_replications = tillers_replications # logging and data structures self.stored_times = stored_times - self.shared_elements_inputs_outputs_df = pd.DataFrame() - self.shared_elements_inputs_outputs_df = pd.DataFrame() - self.shared_hiddenzones_inputs_outputs_df = pd.DataFrame() + self.shared_axes_inputs_outputs_df = pd.DataFrame() self.shared_organs_inputs_outputs_df = pd.DataFrame() + self.shared_hiddenzones_inputs_outputs_df = pd.DataFrame() + self.shared_elements_inputs_outputs_df = pd.DataFrame() self.shared_soils_inputs_outputs_df = pd.DataFrame() - self.shared_axes_inputs_outputs_df = pd.DataFrame() self.all_simulation_steps = [] self.axes_all_data_list = [] self.organs_all_data_list = [] @@ -141,18 +149,29 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.elements_all_data_list = [] self.soils_all_data_list = [] - # boolean choices self.show_3Dplant = show_3Dplant self.heterogeneous_canopy = heterogeneous_canopy self.computing_light_interception = computing_light_interception + self.hydraulics = hydraulics self.option_static = option_static # -- ADEL and MTG CONFIGURATION -- # read adelwheat inputs at t0 - self.adel_wheat = AdelDyn(seed=1, scene_unit='m', leaves=echap_leaves(xy_model='Soissons_byleafclass')) - self.g = self.adel_wheat.load(directory=INPUTS_DIRPATH) + if single_plant: + # TODO introduce the boolean + self.adel_wheat = AdelDyn(seed=1, scene_unit='m', leaves=echap_leaves(xy_model='Soissons_byleafclass')) + else: + stand = AgronomicStand(sowing_density=self.plant_density[1], plant_density=self.plant_density[1], inter_row=inter_row, noise=0.) #todo to be adapted if multiple cultivars + self.adel_wheat = AdelDyn(seed=1, scene_unit='m', leaves=echap_leaves(xy_model='Soissons_byleafclass'), stand=stand) + + # MTG generation + if step_callback is not None and 'ADEL_mtg' in step_callback.keys(): + nff = update_parameters_all_models['morphogenesis']['max_nb_leaves'] + self.g = step_callback['ADEL_mtg'](adel_wheat, INPUTS_DIRPATH, nff) # Create a new MTG + else: + self.g = adel_wheat.load(directory=INPUTS_DIRPATH) # read adelwheat inputs at t0 from a serialised MTG # Section specific to coupling with Root-BRIDGES self.shoot_props = self.g.properties() @@ -166,36 +185,34 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, # ----- CONFIGURATION OF THE FACADES ------- # --------------------------------------------- - # -- ELONGWHEAT (created first because it is the only facade to add new metamers) -- + # -- MORPHOGENESIS (created first because it is the only facade to add new metamers) -- # Initial states - elongwheat_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME][ - elongwheat_facade.converter.HIDDENZONE_TOPOLOGY_COLUMNS + [i for i in elongwheat_facade.simulation.HIDDENZONE_INPUTS if i in - inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME].columns]].copy() - elongwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ - elongwheat_facade.converter.ELEMENT_TOPOLOGY_COLUMNS + [i for i in elongwheat_facade.simulation.ELEMENT_INPUTS if i in - inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() - elongwheat_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ - elongwheat_facade.converter.AXIS_TOPOLOGY_COLUMNS + [i for i in elongwheat_facade.simulation.AXIS_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + morphogenesis_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME] + morphogenesis_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME] + morphogenesis_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME] phytoT_df = pd.read_csv(os.path.join(INPUTS_DIRPATH, 'phytoT.csv')) # Update parameters if specified - if update_parameters_all_models and 'elongwheat' in update_parameters_all_models: - update_parameters_elongwheat = update_parameters_all_models['elongwheat'] + if update_parameters_all_models and 'morphogenesis' in update_parameters_all_models: + update_parameters_morphogenesis = update_parameters_all_models['morphogenesis'] else: - update_parameters_elongwheat = None + update_parameters_morphogenesis = None # Facade initialisation - self.elongwheat_facade_ = elongwheat_facade.ElongWheatFacade(self.g, + self.morphogenesis_facade_ = morphogenesis_facade.MorphogenesisFacade(self.g, MORPHOGENESIS_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, - elongwheat_axes_initial_state, - elongwheat_hiddenzones_initial_state, - elongwheat_elements_initial_state, + morphogenesis_axes_initial_state, + morphogenesis_hiddenzones_initial_state, + morphogenesis_elements_initial_state, self.shared_axes_inputs_outputs_df, self.shared_hiddenzones_inputs_outputs_df, self.shared_elements_inputs_outputs_df, self.adel_wheat, phytoT_df, - update_parameters_elongwheat, + hydraulics=hydraulics, + optimal_growth_option=optimal_growth_option, + option_static=option_static, + update_parameters=update_parameters_morphogenesis, update_shared_df=UPDATE_SHARED_DF) # -- CARIBU -- @@ -205,149 +222,210 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.adel_wheat, update_shared_df=UPDATE_SHARED_DF) - # -- SENESCWHEAT -- + # -- SENESCENCE -- # Initial states - senescwheat_roots_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].loc[inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME]['organ'] == 'roots'][ - senescwheat_facade.converter.ROOTS_TOPOLOGY_COLUMNS + - [i for i in senescwheat_facade.converter.SENESCWHEAT_ROOTS_INPUTS if i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() + senescence_roots_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].loc[inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME]['organ'] == 'roots'][ + senescence_facade.converter.ROOTS_TOPOLOGY_COLUMNS + + [i for i in senescence_facade.converter.SENESCENCE_ROOTS_INPUTS if i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() - senescwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ - senescwheat_facade.converter.ELEMENTS_TOPOLOGY_COLUMNS + - [i for i in senescwheat_facade.converter.SENESCWHEAT_ELEMENTS_INPUTS if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() + senescence_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ + senescence_facade.converter.ELEMENTS_TOPOLOGY_COLUMNS + + [i for i in senescence_facade.converter.SENESCENCE_ELEMENTS_INPUTS if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() - senescwheat_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ - senescwheat_facade.converter.AXES_TOPOLOGY_COLUMNS + - [i for i in senescwheat_facade.converter.SENESCWHEAT_AXES_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + senescence_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ + senescence_facade.converter.AXES_TOPOLOGY_COLUMNS + + [i for i in senescence_facade.converter.SENESCENCE_AXES_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() # Update parameters if specified - if update_parameters_all_models and 'senescwheat' in update_parameters_all_models: - update_parameters_senescwheat = update_parameters_all_models['senescwheat'] + if update_parameters_all_models and 'senescence' in update_parameters_all_models: + update_parameters_senescence = update_parameters_all_models['senescence'] else: - update_parameters_senescwheat = None + update_parameters_senescence = None # Facade initialisation - self.senescwheat_facade_ = senescwheat_facade.SenescWheatFacade(self.g, + self.senescence_facade_ = senescence_facade.SENESCENCEFacade(self.g, SENESCENCE_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, - senescwheat_roots_initial_state, - senescwheat_axes_initial_state, - senescwheat_elements_initial_state, + senescence_roots_initial_state, + senescence_axes_initial_state, + senescence_elements_initial_state, self.shared_organs_inputs_outputs_df, self.shared_axes_inputs_outputs_df, self.shared_elements_inputs_outputs_df, - update_parameters_senescwheat, + update_parameters=update_parameters_senescence, update_shared_df=UPDATE_SHARED_DF, - cnwheat_roots=cnwheat_roots) + cnwgrass_roots=cnwgrass_roots) - # -- FARQUHARWHEAT -- - # Initial states - farquharwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ - farquharwheat_facade.converter.ELEMENT_TOPOLOGY_COLUMNS + - [i for i in farquharwheat_facade.converter.FARQUHARWHEAT_ELEMENTS_INPUTS if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() - - farquharwheat_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ - farquharwheat_facade.converter.AXIS_TOPOLOGY_COLUMNS + - [i for i in farquharwheat_facade.converter.FARQUHARWHEAT_AXES_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + # -- GAS-EXCHANGE -- + # Initial states + gasexchange_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME] + gasexchange_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME] - # Use the initial version of the photosynthesis sub-model (as in Barillot et al. 2016, and in Gauthier et al. 2020) - update_parameters_farquharwheat = {'SurfacicProteins': False, 'NSC_Retroinhibition': False} + # Update parameters if specified + if update_parameters_all_models and 'gasexchange' in update_parameters_all_models: + update_parameters_gasexchange = update_parameters_all_models['gasexchange'] + else: + update_parameters_gasexchange = None # Facade initialisation - self.farquharwheat_facade_ = farquharwheat_facade.FarquharWheatFacade(self.g, - farquharwheat_elements_initial_state, - farquharwheat_axes_initial_state, + self.gasexchange_facade_ = gasexchange_facade.GasExchangeFacade(self.g, + gasexchange_elements_initial_state, + gasexchange_axes_initial_state, self.shared_elements_inputs_outputs_df, - update_parameters_farquharwheat, + stomatal_model_name=stomatal_model_name, + hydraulics=hydraulics, + update_parameters=update_parameters_farquharwheat, update_shared_df=UPDATE_SHARED_DF) - # -- GROWTHWHEAT -- - # Initial states - growthwheat_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME][ - growthwheat_facade.converter.HIDDENZONE_TOPOLOGY_COLUMNS + - [i for i in growthwheat_facade.simulation.HIDDENZONE_INPUTS if i in inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME].columns]].copy() - - growthwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ - growthwheat_facade.converter.ELEMENT_TOPOLOGY_COLUMNS + - [i for i in growthwheat_facade.simulation.ELEMENT_INPUTS if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() - - growthwheat_root_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].loc[inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME]['organ'] == 'roots'][ - growthwheat_facade.converter.ROOT_TOPOLOGY_COLUMNS + - [i for i in growthwheat_facade.simulation.ROOT_INPUTS if i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() - - growthwheat_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ - growthwheat_facade.converter.AXIS_TOPOLOGY_COLUMNS + - [i for i in growthwheat_facade.simulation.AXIS_INPUTS if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + # -- GROWTH -- + # Initial states + growth_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME] + growth_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME] + growth_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME] + growth_root_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME] # Update parameters if specified - if update_parameters_all_models and 'growthwheat' in update_parameters_all_models: - update_parameters_growthwheat = update_parameters_all_models['growthwheat'] + if update_parameters_all_models and 'growth' in update_parameters_all_models: + update_parameters_growth = update_parameters_all_models['growth'] else: - update_parameters_growthwheat = None + update_parameters_growth = None # Facade initialisation - self.growthwheat_facade_ = growthwheat_facade.GrowthWheatFacade(self.g, + self.growth_facade_ = growth_facade.GrowthWheatFacade(self.g, GROWTH_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, - growthwheat_hiddenzones_initial_state, - growthwheat_elements_initial_state, - growthwheat_root_initial_state, - growthwheat_axes_initial_state, + growth_hiddenzones_initial_state, + growth_elements_initial_state, + growth_root_initial_state, + growth_axes_initial_state, self.shared_organs_inputs_outputs_df, self.shared_hiddenzones_inputs_outputs_df, self.shared_elements_inputs_outputs_df, self.shared_axes_inputs_outputs_df, - update_parameters_growthwheat, + hydraulics=hydraulics, + update_parameters=update_parameters_growthwheat, update_shared_df=UPDATE_SHARED_DF, - cnwheat_roots=cnwheat_roots) + cnwgrass_roots=cnwgrass_roots) - # -- CNWHEAT -- - # Initial states - cnwheat_organs_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME][ - [i for i in cnwheat_facade.cnwheat_converter.ORGANS_VARIABLES if i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() + # -- CNMETABOLISM -- + # Initial states + cnmetabolism_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ + [i for i in cnmetabolism_facade.cnmetabolism_converter.AXES_VARIABLES if i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + + cnmetabolism_organs_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME][ + [i for i in cnmetabolism_facade.cnmetabolism_converter.ORGANS_VARIABLES if i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() - cnwheat_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME][ - [i for i in cnwheat_facade.cnwheat_converter.HIDDENZONE_VARIABLES if i in inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME].columns]].copy() + cnmetabolism_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME][ + [i for i in cnmetabolism_facade.cnmetabolism_converter.HIDDENZONE_VARIABLES if i in inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME].columns]].copy() - cnwheat_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ - [i for i in cnwheat_facade.cnwheat_converter.ELEMENTS_VARIABLES if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() + cnmetabolism_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ + [i for i in cnmetabolism_facade.cnmetabolism_converter.ELEMENTS_VARIABLES if i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() - cnwheat_soils_initial_state = inputs_dataframes[SOILS_INITIAL_STATE_FILENAME][ - [i for i in cnwheat_facade.cnwheat_converter.SOILS_VARIABLES if i in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns]].copy() + cnmetabolism_soils_initial_state = inputs_dataframes[SOILS_INITIAL_STATE_FILENAME][ + [i for i in cnmetabolism_facade.cnmetabolism_converter.SOILS_VARIABLES if i in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns]].copy() + if not hydraulics and 'SRWC' not in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns: + cnmetabolism_soils_initial_state['SRWC'] = 100 + elif hydraulics and 'SRWC' not in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns: + raise(ValueError('Hydraulics option is True but SRWC not found in {}.'.format(SOILS_INITIAL_STATE_FILENAME))) # Update parameters if specified - if update_parameters_all_models and 'cnwheat' in update_parameters_all_models: - update_parameters_cnwheat = update_parameters_all_models['cnwheat'] + if update_parameters_all_models and 'cnmetabolism' in update_parameters_all_models: + update_parameters_cnmetabolism = update_parameters_all_models['cnmetabolism'] else: - update_parameters_cnwheat = {} + update_parameters_cnmetabolism = {} # Force solver separation if a different root model has been chosen - if not cnwheat_roots: + if not cnwgrass_roots: isolated_roots = True # Facade initialisation - self.cnwheat_facade_ = cnwheat_facade.CNWheatFacade(self.g, + self.cnmetabolism_facade_ = cnmetabolism_facade.CNMetabolismFacade(self.g, CNMETABOLISM_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, - PLANT_DENSITY, - update_parameters_cnwheat, - cnwheat_organs_initial_state, - cnwheat_hiddenzones_initial_state, - cnwheat_elements_initial_state, - cnwheat_soils_initial_state, + plant_density, + update_parameters_cnmetabolism, + cnmetabolism_axes_initial_state, + cnmetabolism_organs_initial_state, + cnmetabolism_hiddenzones_initial_state, + cnmetabolism_elements_initial_state, + cnmetabolism_soils_initial_state, self.shared_axes_inputs_outputs_df, self.shared_organs_inputs_outputs_df, self.shared_hiddenzones_inputs_outputs_df, self.shared_elements_inputs_outputs_df, self.shared_soils_inputs_outputs_df, + tillers_replications=tillers_replications, + external_soil_model=external_soil_model, update_shared_df=UPDATE_SHARED_DF, isolated_roots=isolated_roots, - cnwheat_roots=cnwheat_roots) + cnwgrass_roots=cnwgrass_roots) # Run cnwheat with constant nitrates concentration in the soil if specified - if N_fertilizations is not None and 'constant_Conc_Nitrates' in N_fertilizations.keys(): - self.cnwheat_facade_.soils[(1, 'MS')].constant_Conc_Nitrates = True # TODO: make (1, 'MS') more general - self.cnwheat_facade_.soils[(1, 'MS')].nitrates = N_fertilizations['constant_Conc_Nitrates'] * self.cnwheat_facade_.soils[(1, 'MS')].volume - - # -- FSPMWHEAT -- + if N_fertilizations is not None: + if 'constant_Conc_Nitrates' in N_fertilizations.keys(): + self.cnmetabolism_facade_.soils[(1, 'MS')].constant_Conc_Nitrates = True # TODO: make (1, 'MS') more general + self.cnmetabolism_facade_.soils[(1, 'MS')].nitrates = N_fertilizations['constant_Conc_Nitrates'] * self.cnmetabolism_facade_.soils[(1, 'MS')].volume + + # Force root nitrate uptake if specified + if external_soil_model and step_callback is not None: + try: + step_callback['nitrate_uptake'](0, self.cnmetabolism_facade_.population, self.g) + except KeyError: + print('Function name error in step_callback keys. It should be nitrate_uptake') + + + # -- HYDRAULICS -- + drought_ongoing = False # Is a drought event ongoing (bool) + drought_passed = False # Has a drought event occurred (bool) + rehydration = False # Is a rehydration period ongoing (bool) + self.hydraulics_facade_ = None + + if hydraulics: + # Initial states + hydraulics_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME][ + [i for i in hydraulics_facade.hydraulics_converter.AXES_VARIABLES if + i in inputs_dataframes[AXES_INITIAL_STATE_FILENAME].columns]].copy() + + hydraulics_organs_initial_state = inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME][ + [i for i in hydraulics_facade.hydraulics_converter.ORGANS_VARIABLES if + i in inputs_dataframes[ORGANS_INITIAL_STATE_FILENAME].columns]].copy() + + hydraulics_hiddenzones_initial_state = inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME][ + [i for i in hydraulics_facade.hydraulics_converter.HIDDENZONE_VARIABLES if + i in inputs_dataframes[HIDDENZONES_INITIAL_STATE_FILENAME].columns]].copy() + + hydraulics_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME][ + [i for i in hydraulics_facade.hydraulics_converter.ELEMENTS_VARIABLES if + i in inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME].columns]].copy() + + # Update parameters if specified + if update_parameters_all_models and 'hydraulics' in update_parameters_all_models: + update_parameters_hydraulics = update_parameters_all_models['hydraulics'] + else: + update_parameters_hydraulics = {} + + hydraulics_soils_initial_state = inputs_dataframes[SOILS_INITIAL_STATE_FILENAME][ + [i for i in hydraulics_facade.hydraulics_converter.SOILS_VARIABLES if + i in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns]].copy() + + # Facade initialisation + self.hydraulics_facade_ = hydraulics_facade.hydraulicsFacade(g, + HYDRAULICS_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, + update_parameters_hydraulics, + hydraulics_axes_initial_state, + hydraulics_hiddenzones_initial_state, + hydraulics_elements_initial_state, + hydraulics_organs_initial_state, + hydraulics_soils_initial_state, + self.shared_axes_inputs_outputs_df, + self.shared_hiddenzones_inputs_outputs_df, + self.shared_elements_inputs_outputs_df, + self.shared_organs_inputs_outputs_df, + self.shared_soils_inputs_outputs_df, + update_shared_df=UPDATE_SHARED_DF, + isolated_roots=isolated_roots, + cnwgrass_roots=cnwgrass_roots) + + # -- MODEL INTEGRATION -- # Facade initialisation - self.fspmwheat_facade_ = fspmwheat_facade.FSPMWheatFacade(self.g) + build_outputs_ = build_outputs.BuildOutputs(self.g, self.morphogenesis_facade_, self.growth_facade_, self.gasexchange_facade_, self.hydraulics_facade_) # Update geometry self.adel_wheat.update_geometry(self.g) @@ -483,19 +561,11 @@ def __call__(self): self.caribu_facade_.run(run_caribu, energy=PARi, DOY=DOY, hourTU=hour, latitude=48.85, sun_sky_option='sky', heterogeneous_canopy=self.heterogeneous_canopy, plant_density=self.PLANT_DENSITY[1]) - for t_senescwheat in range(self.time_step_in_hours, self.time_step_in_hours + self.SENESCENCE_TIMESTEP, self.SENESCENCE_TIMESTEP): - # run SenescWheat + for t_senescence in range(self.time_step_in_hours, self.time_step_in_hours + self.SENESCENCE_TIMESTEP, self.SENESCENCE_TIMESTEP): + # run Senescence t1 = time.time() - self.senescwheat_facade_.run() - # if (1, 'MS', 5, 'internode', 'HiddenElement') not in self.senescwheat_facade_._simulation.outputs["elements"].keys(): - # logger_output.info("Post SW, Internode not found") - hidelt = self.g.node(80) - if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): - logger_output.info(f"Post SW, Internode element has no proper length in MTG : {hidelt.length}") - hidorg = self.g.node(70) - if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): - logger_output.info(f"Post SW, Internode organ has no proper length in MTG : {hidorg.length}") - if debug: print("senescwheat took :", time.time() - t1) + self.senescence_facade_.run() + if debug: print("senescence took :", time.time() - t1) # Test for dead plant # TODO: adapt in case of multiple plants if not self.shared_elements_inputs_outputs_df.empty and \ @@ -510,89 +580,53 @@ def __call__(self): break # Run the rest of the model if the plant is alive - for t_farquharwheat in range(t_senescwheat, t_senescwheat + self.SENESCENCE_TIMESTEP, self.FARQUHARWHEAT_TIMESTEP): + for t_gasexchange in range(t_senescence, t_senescence + self.SENESCENCE_TIMESTEP, self.FARQUHARWHEAT_TIMESTEP): # TODO what replaced gas exchange timestep? # get the meteo of the current step - Ta, ambient_CO2, RH, Ur = self.meteo.loc[t_farquharwheat, ['air_temperature', 'ambient_CO2', 'humidity', 'Wind']] + Ta, ambient_CO2, RH, Ur = self.meteo.loc[t_gasexchange, ['air_temperature', 'ambient_CO2', 'humidity', 'Wind']] - # run FarquharWheat + # run GasExchange t1 = time.time() - self.farquharwheat_facade_.run(Ta, ambient_CO2, RH, Ur) - # if (1, 'MS', 5, 'internode', 'HiddenElement') not in self.farquharwheat_facade_._simulation.outputs.keys(): - # logger_output.info("Post FW, Internode not found") - hidelt = self.g.node(80) - if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): - logger_output.info(f"Post FW, Internode element has no proper length in MTG : {hidelt.length}") - hidorg = self.g.node(70) - if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): - logger_output.info(f"Post FW, Internode organ has no proper length in MTG : {hidorg.length}") - if debug: print("farquharwheat took :", time.time() - t1) - - for t_elongwheat in range(t_farquharwheat, t_farquharwheat + self.FARQUHARWHEAT_TIMESTEP, self.MORPHOGENESIS_TIMESTEP): - # run ElongWheat - Tair, Tsoil = self.meteo.loc[t_elongwheat, ['air_temperature', 'soil_temperature']] + self.gasexchange_facade_.run(Ta, ambient_CO2, RH, Ur) + if debug: print("gasexchange took :", time.time() - t1) + + for t_morphogenesis in range(t_gasexchange, t_gasexchange + self.FARQUHARWHEAT_TIMESTEP, self.MORPHOGENESIS_TIMESTEP): + # run Morphogeneis + Tair, Tsoil = self.meteo.loc[t_gasexchange, ['air_temperature', 'soil_temperature']] t1 = time.time() - self.elongwheat_facade_.run(Tair, Tsoil, option_static=self.option_static) - # if (1, 'MS', 5, 'internode', 'HiddenElement') not in self.elongwheat_facade_._simulation.outputs["elements"].keys(): - # logger_output.info("Post EW, Internode not found") - hidelt = self.g.node(80) - if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): - logger_output.info(f"Post EW, Internode element has no proper length in MTG : {hidelt.length}") - hidorg = self.g.node(70) - if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): - logger_output.info(f"Post EW, Internode organ has no proper length in MTG : {hidorg.length}") - if debug: print("elongwheat took :", time.time() - t1) + self.morphogenesis_facade_.run(Tair, Tsoil, option_static=self.option_static) + if debug: print("morphogenesis took :", time.time() - t1) # Update geometry self.adel_wheat.update_geometry(self.g) if self.show_3Dplant: self.adel_wheat.plot(self.g) - hidelt = self.g.node(80) - if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): - logger_output.info(f"Post AW, Internode element has no proper length in MTG : {hidelt.length}") - hidorg = self.g.node(70) - if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): - logger_output.info(f"Post AW, Internode organ has no proper length in MTG : {hidorg.length}") - - for t_growthwheat in range(t_elongwheat, t_elongwheat + self.MORPHOGENESIS_TIMESTEP, self.GROWTH_TIMESTEP): - # run GrowthWheat + + for t_growth in range(t_morphogenesis, t_morphogenesis + self.MORPHOGENESIS_TIMESTEP, self.GROWTH_TIMESTEP): + # run Growth t1 = time.time() - self.growthwheat_facade_.run() - # if (1, 'MS', 5, 'internode', 'HiddenElement') not in self.growthwheat_facade_._simulation.outputs["elements"].keys(): - # logger_output.info("Post GW, Internode not found") - hidelt = self.g.node(80) - if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): - logger_output.info(f"Post GW, Internode element has no proper length in MTG : {hidelt.length}") - hidorg = self.g.node(70) - if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): - logger_output.info(f"Post GW, Internode organ has no proper length in MTG : {hidorg.length}") - - for t_cnwheat in range(t_growthwheat, t_growthwheat + self.GROWTH_TIMESTEP, self.CNMETABOLISM_TIMESTEP): - #print('t cnwheat is {}'.format(t_cnwheat)) + self.growth_facade_.run() + if debug: print("growth took :", time.time() - t1) + + for t_cnmetabolism in range(t_growth, t_growth + self.GROWTH_TIMESTEP, self.CNMETABOLISM_TIMESTEP): # N fertilization if any if self.N_fertilizations is not None and len(self.N_fertilizations) > 0: - if t_cnwheat in self.N_fertilizations.keys(): - self.cnwheat_facade_.soils[(1, 'MS')].nitrates += self.N_fertilizations[t_cnwheat] + if t_cnmetabolism in self.N_fertilizations.keys(): + self.cnmetabolism_facade_.soils[(1, 'MS')].nitrates += self.N_fertilizations[t_cnmetabolism] if t_cnwheat > 0: # run CNWheat Tair = self.meteo.loc[t_elongwheat, 'air_temperature'] Tsoil = self.meteo.loc[t_elongwheat, 'soil_temperature'] t1 = time.time() - self.cnwheat_facade_.run(Tair, Tsoil, self.tillers_replications) - if debug: print("cnwheat took :", time.time() - t1) - hidelt = self.g.node(80) - if (hidelt.length is None) or (np.isnan(hidelt.length)) or (np.isinf(hidelt.length)) or (hidelt.length <= 0.): - logger_output.info(f"Post CNW, Internode element has no proper length in MTG : {hidelt.length}") - hidorg = self.g.node(70) - if (hidorg.length is None) or (np.isnan(hidorg.length)) or (np.isinf(hidorg.length)) or (hidorg.length <= 0.): - logger_output.info(f"Post CNW, Internode organ has no proper length in MTG : {hidorg.length}") + self.cnmetabolism_facade_.run(Tair, Tsoil, self.tillers_replications) + if debug: print("cnmetabolism took :", time.time() - t1) # append outputs at current step to global lists - if (self.stored_times == 'all') or (t_cnwheat in self.stored_times): - axes_outputs, elements_outputs, hiddenzones_outputs, organs_outputs, soils_outputs = self.fspmwheat_facade_.build_outputs_df_from_MTG() + if (self.stored_times == 'all') or (t_cnmetabolism in self.stored_times): + axes_outputs, elements_outputs, hiddenzones_outputs, organs_outputs, soils_outputs = self.fspmwheat_facade_.build_outputs_df_from_MTG() # TODO - self.all_simulation_steps.append(t_cnwheat) + self.all_simulation_steps.append(t_cnmetabolism) self.axes_all_data_list.append(axes_outputs) self.organs_all_data_list.append(organs_outputs) self.hiddenzones_all_data_list.append(hiddenzones_outputs) @@ -608,7 +642,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}, stored_times = None, option_static = False, show_3Dplant = False, run_from_outputs = False, heterogeneous_canopy = True, update_parameters_all_models = None, - isolated_roots = False, cnwheat_roots = True): + isolated_roots = False, cnwgrass_roots = True): scenario = {} ### DIRS ### @@ -619,7 +653,7 @@ def scenario_utility(time_step_in_seconds: int = 3600, INPUTS_DIRPATH = "inputs" UPDATE_SHARED_DF = False if stored_times is None: stored_times = 'all' - if not (stored_times == 'all' or type(stored_times) == list): + if not (stored_times == 'all' or isinstance(stored_times, list)): print('stored_times should be either \'all\', a list or an empty list.') raise @@ -720,7 +754,7 @@ def scenario_utility(time_step_in_seconds: int = 3600, INPUTS_DIRPATH = "inputs" scenario["N_fertilizations"] = N_fertilizations scenario["update_parameters_all_models"] = update_parameters_all_models scenario["isolated_roots"] = isolated_roots - scenario["cnwheat_roots"] = cnwheat_roots + scenario["cnwgrass_roots"] = cnwgrass_roots return scenario From 9f457a942b033e69c41adc74f6098c4aaadfd2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 6 Aug 2026 16:23:34 +0200 Subject: [PATCH 03/21] component wrapping --- .../cnwgrass/integration/component.py | 276 +++++++++--------- src/openalea/cnwgrass/integration/runner.py | 2 - 2 files changed, 144 insertions(+), 134 deletions(-) diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index 3fd2c60..971bf72 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -3,13 +3,10 @@ import os import numpy as np import pandas as pd -import time -import logging from dataclasses import dataclass, fields from openalea.metafspm.component import Model, declare - from openalea.adel.adel_dynamic import AdelDyn from openalea.adel.echap_leaf import echap_leaves from openalea.cnwgrass.integration import caribu_facade @@ -22,14 +19,21 @@ from openalea.cnwgrass.integration import hydraulics_facade -logger_output = logging.getLogger("Simulation_Logger") - -debug = False - @dataclass class CNW_Grass(Model): """ - TODO : Add description + CNW-Grass is a Functional Structural Plant Model (FSPM) of grasses which fully integrates shoot morphogenesis and the metabolism of carbon (C) and nitrogen (N) at organ scale within a 3D representation of plant architecture. Plants are described as a collection of tillers, each consisting in individual shoot organs (lamina, sheath, internode, peduncle, chaff), a single root compartment, the grains, and a phloem. CNW-Grass also includes a hydraulic model allowing to compute water flow in the plant and the co-regulation of leaf growth by metabolic and hydraulic processes. In this case, the plants also include a xylem compartment. + + CNW-Grass simulates: + + Organ photosynthesis, temperature and transpiration from light distribution within the 3D canopy. + Leaf and internode elongation. + Leaf, internode and root growth in mass. + N acquisition, synthesis and allocation of C and N metabolites at organ level and among tiller organs. + Senescence of shoot organs and roots. + Water fluxes and water potentials. + + Model inputs are the pedoclimatic conditions (temperature, light, humidity, CO2, wind, soil NO3-, Soil Relative Water Content) and initial dimensions, mass and metabolic composition of individual organs. """ # Inputs expected from bellowground models @@ -103,9 +107,12 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, HIDDENZONES_INITIAL_STATE_FILENAME = 'hiddenzones_initial_state.csv', ELEMENTS_INITIAL_STATE_FILENAME = 'elements_initial_state.csv', AXES_INITIAL_STATE_FILENAME = 'axes_initial_state.csv', update_parameters_all_models = None, step_callback=None, HOUR_TO_SECOND_CONVERSION_FACTOR = 3600, ORGANS_INITIAL_STATE_FILENAME = 'organs_initial_state.csv', SOILS_INITIAL_STATE_FILENAME = 'soils_initial_state.csv', INPUTS_DIRPATH='inputs', - MANAGEMENT_FILENAME = 'management.csv', tillers_replications=None, stored_times = None, computing_light_interception=True, heterogeneous_canopy=True, show_3Dplant = False, + # 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, 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, + 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): # SELF STORAGE FOR LOOP PARAMETERS @@ -122,15 +129,10 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, # canopy parameters # Management data - management_df = pd.read_csv(os.path.join(INPUTS_DIRPATH, MANAGEMENT_FILENAME), header=0, index_col=0) - management_variables = {} - for var_name in management_df.index: - management_variables[var_name] = get_management_value(management_df, var_name) - self.plant_density = management_variables.get('plant_density', {1: 250}) - self.inter_row = management_variables.get('inter_row', 0.15) - self.Zsowing = management_variables.get('sowing_depth', 0.025) - self.N_fertilizations = management_variables.get('N_fertilizations', {}) - # TODO choose between scenario passing or management dependancy + self.plant_density = plant_density + self.inter_row = inter_row + self.Zsowing = sowing_depth + self.N_fertilizations = N_fertilizations # plant parameters self.tillers_replications = tillers_replications @@ -155,12 +157,12 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.computing_light_interception = computing_light_interception self.hydraulics = hydraulics self.option_static = option_static + self.cnwgrass_root = cnwgrass_roots # -- ADEL and MTG CONFIGURATION -- # read adelwheat inputs at t0 if single_plant: - # TODO introduce the boolean self.adel_wheat = AdelDyn(seed=1, scene_unit='m', leaves=echap_leaves(xy_model='Soissons_byleafclass')) else: stand = AgronomicStand(sowing_density=self.plant_density[1], plant_density=self.plant_density[1], inter_row=inter_row, noise=0.) #todo to be adapted if multiple cultivars @@ -339,7 +341,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, # Facade initialisation self.cnmetabolism_facade_ = cnmetabolism_facade.CNMetabolismFacade(self.g, CNMETABOLISM_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, - plant_density, + plant_density, # TODO GB check why it is needed update_parameters_cnmetabolism, cnmetabolism_axes_initial_state, cnmetabolism_organs_initial_state, @@ -357,18 +359,19 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, isolated_roots=isolated_roots, cnwgrass_roots=cnwgrass_roots) - # Run cnwheat with constant nitrates concentration in the soil if specified - if N_fertilizations is not None: - if 'constant_Conc_Nitrates' in N_fertilizations.keys(): - self.cnmetabolism_facade_.soils[(1, 'MS')].constant_Conc_Nitrates = True # TODO: make (1, 'MS') more general - self.cnmetabolism_facade_.soils[(1, 'MS')].nitrates = N_fertilizations['constant_Conc_Nitrates'] * self.cnmetabolism_facade_.soils[(1, 'MS')].volume + if cnwgrass_roots: + # Run cnwheat with constant nitrates concentration in the soil if specified + if N_fertilizations is not None: + if 'constant_Conc_Nitrates' in N_fertilizations.keys(): + self.cnmetabolism_facade_.soils[(1, 'MS')].constant_Conc_Nitrates = True # TODO: make (1, 'MS') more general + self.cnmetabolism_facade_.soils[(1, 'MS')].nitrates = N_fertilizations['constant_Conc_Nitrates'] * self.cnmetabolism_facade_.soils[(1, 'MS')].volume - # Force root nitrate uptake if specified - if external_soil_model and step_callback is not None: - try: - step_callback['nitrate_uptake'](0, self.cnmetabolism_facade_.population, self.g) - except KeyError: - print('Function name error in step_callback keys. It should be nitrate_uptake') + # Force root nitrate uptake if specified + if external_soil_model and step_callback is not None: + try: + step_callback['nitrate_uptake'](0, self.cnmetabolism_facade_.population, self.g) + except KeyError: + print('Function name error in step_callback keys. It should be nitrate_uptake') # -- HYDRAULICS -- @@ -425,7 +428,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, # -- MODEL INTEGRATION -- # Facade initialisation - build_outputs_ = build_outputs.BuildOutputs(self.g, self.morphogenesis_facade_, self.growth_facade_, self.gasexchange_facade_, self.hydraulics_facade_) + self.build_outputs_ = build_outputs.BuildOutputs(self.g, self.morphogenesis_facade_, self.growth_facade_, self.gasexchange_facade_, self.hydraulics_facade_) # Update geometry self.adel_wheat.update_geometry(self.g) @@ -434,7 +437,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.cn_wheat_root_props = self.g.get_vertex_property(2)["roots"] - # TODO : Temporary + # TODO GB : Temporary self.cn_wheat_root_props["Unloading_Sucrose"] = self.props["Unloading_Sucrose"][1] self.cn_wheat_root_props["Unloading_Amino_Acids"] = self.props["Unloading_Amino_Acids"][1] self.g.properties()["Total_Transpiration"][2] = self.props["Total_Transpiration"][1] @@ -471,7 +474,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.props["adventitious_to_emerge"].update({1: nodal_emergence_delays}) - # TODO Temporary while an initialization bug for shared mtg is still present + # TODO GB Temporary while an initialization bug for shared mtg is still present self.g.get_vertex_property(2)['phloem']['Unloading_Sucrose_shoot_organs'] = 30. self.g.get_vertex_property(2)['phloem']['Unloading_Amino_Acids_shoot_organs'] = 1. @@ -551,6 +554,7 @@ def __call__(self): DOY = self.meteo.loc[self.time_step_in_hours, ['DOY']].iloc[0] hour = self.meteo.loc[self.time_step_in_hours, ['hour']].iloc[0] + # Run Caribu, if activated if self.computing_light_interception: PARi_next_hours = self.meteo.loc[range(self.time_step_in_hours, self.time_step_in_hours + self.CARIBU_TIMESTEP), ['PARi']].sum().values[0] @@ -559,79 +563,107 @@ def __call__(self): else: run_caribu = False - self.caribu_facade_.run(run_caribu, energy=PARi, DOY=DOY, hourTU=hour, latitude=48.85, sun_sky_option='sky', heterogeneous_canopy=self.heterogeneous_canopy, plant_density=self.PLANT_DENSITY[1]) - - for t_senescence in range(self.time_step_in_hours, self.time_step_in_hours + self.SENESCENCE_TIMESTEP, self.SENESCENCE_TIMESTEP): - # run Senescence - t1 = time.time() - self.senescence_facade_.run() - if debug: print("senescence took :", time.time() - t1) - - # Test for dead plant # TODO: adapt in case of multiple plants - if not self.shared_elements_inputs_outputs_df.empty and \ - np.nansum(self.shared_elements_inputs_outputs_df.loc[self.shared_elements_inputs_outputs_df['element'].isin(['StemElement', 'LeafElement1']), 'green_area']) == 0: - # append the inputs and outputs at current step to global lists - self.all_simulation_steps.append(t_senescwheat) - self.axes_all_data_list.append(self.shared_axes_inputs_outputs_df.copy()) - self.organs_all_data_list.append(self.shared_organs_inputs_outputs_df.copy()) - self.hiddenzones_all_data_list.append(self.shared_hiddenzones_inputs_outputs_df.copy()) - self.elements_all_data_list.append(self.shared_elements_inputs_outputs_df.copy()) - self.soils_all_data_list.append(self.shared_soils_inputs_outputs_df.copy()) - break - - # Run the rest of the model if the plant is alive - for t_gasexchange in range(t_senescence, t_senescence + self.SENESCENCE_TIMESTEP, self.FARQUHARWHEAT_TIMESTEP): # TODO what replaced gas exchange timestep? - # get the meteo of the current step - Ta, ambient_CO2, RH, Ur = self.meteo.loc[t_gasexchange, ['air_temperature', 'ambient_CO2', 'humidity', 'Wind']] - - # run GasExchange - t1 = time.time() - self.gasexchange_facade_.run(Ta, ambient_CO2, RH, Ur) - if debug: print("gasexchange took :", time.time() - t1) - - for t_morphogenesis in range(t_gasexchange, t_gasexchange + self.FARQUHARWHEAT_TIMESTEP, self.MORPHOGENESIS_TIMESTEP): - # run Morphogeneis - Tair, Tsoil = self.meteo.loc[t_gasexchange, ['air_temperature', 'soil_temperature']] - t1 = time.time() - self.morphogenesis_facade_.run(Tair, Tsoil, option_static=self.option_static) - if debug: print("morphogenesis took :", time.time() - t1) - - # Update geometry - self.adel_wheat.update_geometry(self.g) - if self.show_3Dplant: - self.adel_wheat.plot(self.g) - - for t_growth in range(t_morphogenesis, t_morphogenesis + self.MORPHOGENESIS_TIMESTEP, self.GROWTH_TIMESTEP): - # run Growth - t1 = time.time() - self.growth_facade_.run() - if debug: print("growth took :", time.time() - t1) - - for t_cnmetabolism in range(t_growth, t_growth + self.GROWTH_TIMESTEP, self.CNMETABOLISM_TIMESTEP): - - # N fertilization if any - if self.N_fertilizations is not None and len(self.N_fertilizations) > 0: - if t_cnmetabolism in self.N_fertilizations.keys(): - self.cnmetabolism_facade_.soils[(1, 'MS')].nitrates += self.N_fertilizations[t_cnmetabolism] - - if t_cnwheat > 0: - # run CNWheat - Tair = self.meteo.loc[t_elongwheat, 'air_temperature'] - Tsoil = self.meteo.loc[t_elongwheat, 'soil_temperature'] - t1 = time.time() - self.cnmetabolism_facade_.run(Tair, Tsoil, self.tillers_replications) - if debug: print("cnmetabolism took :", time.time() - t1) - - # append outputs at current step to global lists - if (self.stored_times == 'all') or (t_cnmetabolism in self.stored_times): - axes_outputs, elements_outputs, hiddenzones_outputs, organs_outputs, soils_outputs = self.fspmwheat_facade_.build_outputs_df_from_MTG() # TODO - - self.all_simulation_steps.append(t_cnmetabolism) - self.axes_all_data_list.append(axes_outputs) - self.organs_all_data_list.append(organs_outputs) - self.hiddenzones_all_data_list.append(hiddenzones_outputs) - self.elements_all_data_list.append(elements_outputs) - self.soils_all_data_list.append(soils_outputs) + self.caribu_facade_.run(run_caribu, energy=PARi, DOY=DOY, hourTU=hour, latitude=48.85, sun_sky_option='sky', + heterogeneous_canopy=self.heterogeneous_canopy, plant_density=self.plant_density[1], inter_row=inter_row) + + # Run Senescence + self.senescence_facade_.run() + + # Test for dead plant # TODO: adapt in case of multiple plants + if not self.shared_elements_inputs_outputs_df.empty and \ + np.nansum(self.shared_elements_inputs_outputs_df.loc[self.shared_elements_inputs_outputs_df['element'].isin(['StemElement', 'LeafElement1']), 'green_area']) == 0: + # append the inputs and outputs at current step to global lists + self.all_simulation_steps.append(self.time_step_in_hours) + self.axes_all_data_list.append(self.shared_axes_inputs_outputs_df.copy()) + self.organs_all_data_list.append(self.shared_organs_inputs_outputs_df.copy()) + self.hiddenzones_all_data_list.append(self.shared_hiddenzones_inputs_outputs_df.copy()) + self.elements_all_data_list.append(self.shared_elements_inputs_outputs_df.copy()) + self.soils_all_data_list.append(self.shared_soils_inputs_outputs_df.copy()) + print('Dead plant') + break + + # Run the rest of the model if the plant is alive + # get the meteo of the current step + Ta, ambient_CO2, RH, Ur = self.meteo.loc[self.time_step_in_hours, ['air_temperature', 'ambient_CO2', 'humidity', 'Wind']] + + # run GasExchange + self.gasexchange_facade_.run(Ta, ambient_CO2, RH, Ur) + + # run Morphogeneis + Tair, Tsoil = self.meteo.loc[self.time_step_in_hours, ['air_temperature', 'soil_temperature']] + self.morphogenesis_facade_.run(Tair, Tsoil, self.Zsowing) # TODO GB why is it needed? + + # Update geometry + self.adel_wheat.update_geometry(self.g) + if self.show_3Dplant: + self.adel_wheat.plot(self.g) + + # run hydraulics + if self.hydraulics and self.hydraulics_facade_ is not None: + if self.cnwgrass_root: + turgor_soil = 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: + drought_ongoing = True + turgor_soil.constant_water_content = False + # Rehydration scenario. Only implemented for a hourly and linear rehydration scenario. + if rehydration_scenario is not None: + # Maximum of drought, start of rehydration + if turgor_soil.SRWC <= rehydration_scenario['stop_drought_SRWC'] and not rehydration: + rehydration = True + total_irrigation = (rehydration_scenario['SRWC_target'] * turgor_soil.PARAMETERS.AWC) / 100 - turgor_soil.water_content # Total amount of water to add to the soil in order to reach the target SRWC + turgor_soil.hourly_irrigation = total_irrigation / (rehydration_scenario['rehydration_duration'] * 24) # Amount of water to add each hour to reach the target SRWC at the end of the rehydration period + + # Ongoing rehydration + elif rehydration: + # Target SRWC reached after rehydration, end of drought event + if turgor_soil.SRWC >= rehydration_scenario['SRWC_target']: + rehydration = False + drought_ongoing = False + drought_passed = True + turgor_soil.water_content = (rehydration_scenario['SRWC_target'] * turgor_soil.PARAMETERS.AWC) / 100 + turgor_soil.SRWC = rehydration_scenario['SRWC_target'] + turgor_soil.constant_water_content = True + turgor_soil.hourly_rehydration = 0 + + self.hydraulics_facade_.run() + + # Update geometry + adel_wheat.update_geometry(g) + if show_3Dplant: + adel_wheat.plot(g) + + # run Growth + self.growth_facade_.run() + + # run CN-Metabolism + if self.cnwgrass_root: + # N fertilization if any + if self.N_fertilizations is not None: + if self.time_step_in_hours in self.N_fertilizations.keys(): + self.cnmetabolism_facade_.soils[(1, 'MS')].nitrates += self.N_fertilizations[self.time_step_in_hours] + + # Force root nitrate uptake if specified + if external_soil_model and step_callback is not None: + try: + step_callback['nitrate_uptake'](self.time_step_in_hours, cnmetabolism_facade_.population, g) + except KeyError: + print( + 'Function name error in step_callback keys. It should be nitrate_uptake') + + self.cnmetabolism_facade_.run(Tair, Tsoil, self.tillers_replications) + + # append outputs at current step to global lists + if (self.stored_times == 'all') or (self.time_step_in_hours in self.stored_times): + axes_outputs, elements_outputs, hiddenzones_outputs, organs_outputs, soils_outputs = self.build_outputs_.build_outputs_df_from_MTG() + + self.all_simulation_steps.append(self.time_step_in_hours) + self.axes_all_data_list.append(axes_outputs) + self.organs_all_data_list.append(organs_outputs) + self.hiddenzones_all_data_list.append(hiddenzones_outputs) + self.elements_all_data_list.append(elements_outputs) + self.soils_all_data_list.append(soils_outputs) self.time_step_in_hours += self.SENESCENCE_TIMESTEP @@ -639,9 +671,9 @@ 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}, +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}, - stored_times = None, option_static = False, show_3Dplant = False, run_from_outputs = False, heterogeneous_canopy = True, update_parameters_all_models = None, + 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): scenario = {} @@ -746,37 +778,17 @@ def scenario_utility(time_step_in_seconds: int = 3600, INPUTS_DIRPATH = "inputs" scenario["inputs_dataframes"] = inputs_dataframes ### OPTIONS ### - scenario["PLANT_DENSITY"] = PLANT_DENSITY + scenario["plant_density"] = plant_density scenario["option_static"] = option_static scenario["show_3Dplant"] = show_3Dplant scenario["tillers_replications"] = tillers_replications scenario["heterogeneous_canopy"] = heterogeneous_canopy scenario["N_fertilizations"] = N_fertilizations scenario["update_parameters_all_models"] = update_parameters_all_models + scenario["single_plant"] = single_plant scenario["isolated_roots"] = isolated_roots scenario["cnwgrass_roots"] = cnwgrass_roots return scenario - -def save_df_to_csv(df, outputs_filepath, precision): - """ - Write outputs of the model - - :param pandas.DataFrame df: the current output dataframe - :param str outputs_filepath: path of the output file - :param int precision: number of digit - - """ - - try: - df.to_csv(outputs_filepath, na_rep='NA', index=False, float_format='%.{}f'.format(precision)) - except IOError as err: - path, filename = os.path.split(outputs_filepath) - filename = os.path.splitext(filename)[0] - newfilename = 'ACTUAL_{}.csv'.format(filename) - newpath = os.path.join(path, newfilename) - df.to_csv(newpath, na_rep='NA', index=False, float_format='%.{}f'.format(precision)) - warnings.warn('[{}] {}'.format(err.errno, err.strerror)) - warnings.warn('File will be saved at {}'.format(newpath)) \ No newline at end of file diff --git a/src/openalea/cnwgrass/integration/runner.py b/src/openalea/cnwgrass/integration/runner.py index 60328e4..d1c0f3c 100644 --- a/src/openalea/cnwgrass/integration/runner.py +++ b/src/openalea/cnwgrass/integration/runner.py @@ -151,7 +151,6 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin GROWTH_TIMESTEP = 1 CNMETABOLISM_TIMESTEP = 1 hydraulics_TIMESTEP = 1 - # Done # precision of floats used to write and format the output CSV files OUTPUTS_PRECISION = 8 @@ -224,7 +223,6 @@ def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessin # Start time of the simulation START_TIME = max(0, new_start_time) - # Done # Name of the CSV files which contains the meteo data meteo = pd.read_csv(os.path.join(INPUTS_DIRPATH, METEO_FILENAME), index_col='t') From d77dfd5c4bb0a7685bf6219d45b46ff548d3e858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 6 Aug 2026 16:39:41 +0200 Subject: [PATCH 04/21] wheat name omissions --- .../cnwgrass/integration/component.py | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index 971bf72..ea1e97b 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -171,9 +171,9 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, # MTG generation if step_callback is not None and 'ADEL_mtg' in step_callback.keys(): nff = update_parameters_all_models['morphogenesis']['max_nb_leaves'] - self.g = step_callback['ADEL_mtg'](adel_wheat, INPUTS_DIRPATH, nff) # Create a new MTG + self.g = step_callback['ADEL_mtg'](self.adel_wheat, INPUTS_DIRPATH, nff) # Create a new MTG else: - self.g = adel_wheat.load(directory=INPUTS_DIRPATH) # read adelwheat inputs at t0 from a serialised MTG + self.g = self.adel_wheat.load(directory=INPUTS_DIRPATH) # read adelwheat inputs at t0 from a serialised MTG # Section specific to coupling with Root-BRIDGES self.shoot_props = self.g.properties() @@ -275,7 +275,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.shared_elements_inputs_outputs_df, stomatal_model_name=stomatal_model_name, hydraulics=hydraulics, - update_parameters=update_parameters_farquharwheat, + update_parameters=update_parameters_gasexchange, update_shared_df=UPDATE_SHARED_DF) # -- GROWTH -- @@ -292,7 +292,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, update_parameters_growth = None # Facade initialisation - self.growth_facade_ = growth_facade.GrowthWheatFacade(self.g, + self.growth_facade_ = growth_facade.GrowthFacade(self.g, GROWTH_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, growth_hiddenzones_initial_state, growth_elements_initial_state, @@ -303,7 +303,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.shared_elements_inputs_outputs_df, self.shared_axes_inputs_outputs_df, hydraulics=hydraulics, - update_parameters=update_parameters_growthwheat, + update_parameters=update_parameters_growth, update_shared_df=UPDATE_SHARED_DF, cnwgrass_roots=cnwgrass_roots) @@ -360,7 +360,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, cnwgrass_roots=cnwgrass_roots) if cnwgrass_roots: - # Run cnwheat with constant nitrates concentration in the soil if specified + # Run cn-metabolism with constant nitrates concentration in the soil if specified if N_fertilizations is not None: if 'constant_Conc_Nitrates' in N_fertilizations.keys(): self.cnmetabolism_facade_.soils[(1, 'MS')].constant_Conc_Nitrates = True # TODO: make (1, 'MS') more general @@ -435,11 +435,11 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, if show_3Dplant: self.adel_wheat.plot(self.g) - self.cn_wheat_root_props = self.g.get_vertex_property(2)["roots"] + self.cnw_grass_root_props = self.g.get_vertex_property(2)["roots"] # TODO GB : Temporary - self.cn_wheat_root_props["Unloading_Sucrose"] = self.props["Unloading_Sucrose"][1] - self.cn_wheat_root_props["Unloading_Amino_Acids"] = self.props["Unloading_Amino_Acids"][1] + self.cnw_grass_root_props["Unloading_Sucrose"] = self.props["Unloading_Sucrose"][1] + self.cnw_grass_root_props["Unloading_Amino_Acids"] = self.props["Unloading_Amino_Acids"][1] self.g.properties()["Total_Transpiration"][2] = self.props["Total_Transpiration"][1] self.g.get_vertex_property(2)['phloem']['sucrose'] = self.props["sucrose_phloem"][1] @@ -484,12 +484,12 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, def sync_shoot_inputs_with_shoot_mtg(self): for name in self.inputs: if name == "Unloading_Sucrose_phloem": - self.cn_wheat_root_props["Unloading_Sucrose"] = self.props[name][1] / self.props['mstruct'][1] + self.cnw_grass_root_props["Unloading_Sucrose"] = self.props[name][1] / self.props['mstruct'][1] elif name == "Unloading_Amino_Acids_phloem": - self.cn_wheat_root_props["Unloading_Amino_Acids"] = self.props[name][1] / self.props['mstruct'][1] + self.cnw_grass_root_props["Unloading_Amino_Acids"] = self.props[name][1] / self.props['mstruct'][1] else: - self.cn_wheat_root_props[name] = self.props[name][1] + self.cnw_grass_root_props[name] = self.props[name][1] def sync_shoot_outputs_with_root_mtg(self): # Link this specific data structure to self for variables exchange, only for outputs that will be read by other models here. @@ -539,7 +539,7 @@ def sync_shoot_outputs_with_root_mtg(self): self.already_emerged_leaves.append(v) else: - self.props[name].update({1: self.cn_wheat_root_props[name]}) + self.props[name].update({1: self.cnw_grass_root_props[name]}) @@ -599,7 +599,7 @@ def __call__(self): self.adel_wheat.plot(self.g) # run hydraulics - if self.hydraulics and self.hydraulics_facade_ is not None: + if self.hydraulics: if self.cnwgrass_root: turgor_soil = hydraulics_facade_.soils[(1, 'MS')] # Trigger drought @@ -630,9 +630,9 @@ def __call__(self): self.hydraulics_facade_.run() # Update geometry - adel_wheat.update_geometry(g) + self.adel_wheat.update_geometry(g) if show_3Dplant: - adel_wheat.plot(g) + self.adel_wheat.plot(g) # run Growth self.growth_facade_.run() @@ -769,7 +769,6 @@ def scenario_utility(time_step_in_seconds: int = 3600, INPUTS_DIRPATH = "inputs" # TODO constrained to interger for now, and using float, so bellow hour time stepping breaks both for loops and input dataframe accessions time_step_in_hours = int(time_step_in_seconds / 3600) scenario["CARIBU_TIMESTEP"] = time_step_in_hours - scenario["FARQUHARWHEAT_TIMESTEP"] = time_step_in_hours scenario["MORPHOGENESIS_TIMESTEP"] = time_step_in_hours scenario["GROWTH_TIMESTEP"] = time_step_in_hours scenario["CNMETABOLISM_TIMESTEP"] = time_step_in_hours From b5d05a2f95135d7d234bbe4d0e68c0b65749d350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 6 Aug 2026 17:25:54 +0200 Subject: [PATCH 05/21] Adel, Morphogenesis, Caribu and Senescence merged --- .../cnwgrass/integration/component.py | 12 +++--- .../cnwgrass/integration/senescence_facade.py | 37 +++++++++-------- .../cnwgrass/senescence/simulation.py | 40 ++++++++++--------- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index ea1e97b..182b707 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -157,7 +157,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.computing_light_interception = computing_light_interception self.hydraulics = hydraulics self.option_static = option_static - self.cnwgrass_root = cnwgrass_roots + self.cnwgrass_roots = cnwgrass_roots # -- ADEL and MTG CONFIGURATION -- @@ -579,8 +579,7 @@ def __call__(self): self.hiddenzones_all_data_list.append(self.shared_hiddenzones_inputs_outputs_df.copy()) self.elements_all_data_list.append(self.shared_elements_inputs_outputs_df.copy()) self.soils_all_data_list.append(self.shared_soils_inputs_outputs_df.copy()) - print('Dead plant') - break + raise RuntimeError('Dead plant') # Run the rest of the model if the plant is alive # get the meteo of the current step @@ -600,7 +599,7 @@ def __call__(self): # run hydraulics if self.hydraulics: - if self.cnwgrass_root: + if self.cnwgrass_roots: turgor_soil = hydraulics_facade_.soils[(1, 'MS')] # Trigger drought if drought_trigger is not None and 'green_area' in drought_trigger.keys(): @@ -638,7 +637,7 @@ def __call__(self): self.growth_facade_.run() # run CN-Metabolism - if self.cnwgrass_root: + if self.cnwgrass_roots: # N fertilization if any if self.N_fertilizations is not None: if self.time_step_in_hours in self.N_fertilizations.keys(): @@ -674,7 +673,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}, 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): + isolated_roots = False, cnwgrass_roots = True, hydraulics = False): scenario = {} ### DIRS ### @@ -787,6 +786,7 @@ def scenario_utility(time_step_in_seconds: int = 3600, INPUTS_DIRPATH = "inputs" scenario["single_plant"] = single_plant scenario["isolated_roots"] = isolated_roots scenario["cnwgrass_roots"] = cnwgrass_roots + scenario["hydraulics"] = hydraulics return scenario diff --git a/src/openalea/cnwgrass/integration/senescence_facade.py b/src/openalea/cnwgrass/integration/senescence_facade.py index 37e10e6..fb177f9 100644 --- a/src/openalea/cnwgrass/integration/senescence_facade.py +++ b/src/openalea/cnwgrass/integration/senescence_facade.py @@ -45,7 +45,8 @@ def __init__(self, shared_mtg, delta_t, shared_axes_inputs_outputs_df, shared_elements_inputs_outputs_df, update_parameters=None, - update_shared_df=True): + update_shared_df=True, + cnwgrass_roots=True): """ :param openalea.mtg.mtg.MTG shared_mtg: The MTG shared between all models. @@ -61,8 +62,8 @@ def __init__(self, shared_mtg, delta_t, """ self._shared_mtg = shared_mtg #: the MTG shared between all models - - self._simulation = simulation.Simulation(delta_t=delta_t, update_parameters=update_parameters) #: the simulator to use to run the model + 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 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']) @@ -180,21 +181,23 @@ def _update_shared_MTG(self, senescence_roots_data_dict, senescence_axes_data_di continue # update the axis property in the MTG - axis_id = (mtg_plant_index, mtg_axis_label) - if axis_id in senescence_axes_data_dict: - senescence_axis_data_dict = senescence_axes_data_dict[axis_id] - 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 + if self.cnwgrass_roots: + axis_id = (mtg_plant_index, mtg_axis_label) - # 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 + if axis_id in senescence_axes_data_dict: + senescence_axis_data_dict = senescence_axes_data_dict[axis_id] + 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 for mtg_metamer_vid in self._shared_mtg.components_iter(mtg_axis_vid): mtg_metamer_index = int(self._shared_mtg.index(mtg_metamer_vid)) @@ -214,7 +217,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/senescence/simulation.py b/src/openalea/cnwgrass/senescence/simulation.py index 5ae0d82..fd305b7 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): + def __init__(self, delta_t=1, update_parameters=None, cnwgrass_roots=True): #: The inputs of Senescence. #: @@ -41,6 +41,7 @@ def __init__(self, delta_t=1, update_parameters=None): if update_parameters: parameters.__dict__.update(update_parameters) + self.cnwgrass_roots = cnwgrass_roots def initialize(self, inputs): """ Initialize :attr:`inputs` from `inputs`. @@ -71,24 +72,25 @@ def run(self, forced_max_protein_elements=None, opt_full_remob=False, postflower all_axes_inputs = self.inputs['axes'] # Roots - all_roots_inputs = self.inputs['roots'] - all_roots_outputs = self.outputs['roots'] - for roots_inputs_id, roots_inputs_dict in all_roots_inputs.items(): - # Temperature-compensated time (delta_teq) - delta_teq = all_axes_inputs[roots_inputs_id]['delta_teq_roots'] - - # loss of mstruct and Nstruct - rate_mstruct_death, rate_Nstruct_death = model.SenescenceModel.calculate_roots_senescence(roots_inputs_dict['mstruct'], roots_inputs_dict['Nstruct'], postflowering_stages) - relative_delta_mstruct = model.SenescenceModel.calculate_relative_delta_mstruct_roots(rate_mstruct_death, roots_inputs_dict['mstruct'], delta_teq) - delta_mstruct, delta_Nstruct = model.SenescenceModel.calculate_delta_mstruct_root(rate_mstruct_death, rate_Nstruct_death, delta_teq) - # loss of cytokinins (losses of nitrates, amino acids and sucrose are neglected) - loss_cytokinins = model.SenescenceModel.calculate_remobilisation(roots_inputs_dict['cytokinins'], relative_delta_mstruct) - # Update of root outputs - all_roots_outputs[roots_inputs_id] = {'mstruct': roots_inputs_dict['mstruct'] - delta_mstruct, - 'senesced_mstruct': roots_inputs_dict['senesced_mstruct'] + delta_mstruct, - 'rate_mstruct_death': rate_mstruct_death, - 'Nstruct': roots_inputs_dict['Nstruct'] - delta_Nstruct, - 'cytokinins': roots_inputs_dict['cytokinins'] - loss_cytokinins} + if self.cnwgrass_roots: + all_roots_inputs = self.inputs['roots'] + all_roots_outputs = self.outputs['roots'] + for roots_inputs_id, roots_inputs_dict in all_roots_inputs.items(): + # Temperature-compensated time (delta_teq) + delta_teq = all_axes_inputs[roots_inputs_id]['delta_teq_roots'] + + # loss of mstruct and Nstruct + rate_mstruct_death, rate_Nstruct_death = model.SenescenceModel.calculate_roots_senescence(roots_inputs_dict['mstruct'], roots_inputs_dict['Nstruct'], postflowering_stages) + relative_delta_mstruct = model.SenescenceModel.calculate_relative_delta_mstruct_roots(rate_mstruct_death, roots_inputs_dict['mstruct'], delta_teq) + delta_mstruct, delta_Nstruct = model.SenescenceModel.calculate_delta_mstruct_root(rate_mstruct_death, rate_Nstruct_death, delta_teq) + # loss of cytokinins (losses of nitrates, amino acids and sucrose are neglected) + loss_cytokinins = model.SenescenceModel.calculate_remobilisation(roots_inputs_dict['cytokinins'], relative_delta_mstruct) + # Update of root outputs + all_roots_outputs[roots_inputs_id] = {'mstruct': roots_inputs_dict['mstruct'] - delta_mstruct, + 'senesced_mstruct': roots_inputs_dict['senesced_mstruct'] + delta_mstruct, + 'rate_mstruct_death': rate_mstruct_death, + 'Nstruct': roots_inputs_dict['Nstruct'] - delta_Nstruct, + 'cytokinins': roots_inputs_dict['cytokinins'] - loss_cytokinins} # Elements all_elements_inputs = self.inputs['elements'] From b499551744b0c32a2462eb95138aa82adad9de02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 6 Aug 2026 17:36:55 +0200 Subject: [PATCH 06/21] growth merge --- src/openalea/cnwgrass/growth/simulation.py | 69 ++++++++++--------- .../cnwgrass/integration/growth_facade.py | 5 +- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/openalea/cnwgrass/growth/simulation.py b/src/openalea/cnwgrass/growth/simulation.py index 0754b1e..8419c3a 100644 --- a/src/openalea/cnwgrass/growth/simulation.py +++ b/src/openalea/cnwgrass/growth/simulation.py @@ -29,7 +29,7 @@ class Simulation(object): """The Simulation class permits to initialize and run a simulation. """ - def __init__(self, delta_t=1, hydraulics=False, update_parameters=None): + def __init__(self, delta_t=1, hydraulics=False, update_parameters=None, cnwgrass_roots=True): """ :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 @@ -81,6 +81,8 @@ def __init__(self, delta_t=1, hydraulics=False, update_parameters=None): else: warnings.warn(f"Parameter '{key}' is not defined in class self.self.model.parameters.") + self.cnwgrass_roots = cnwgrass_roots + def initialize(self, inputs): """ Initialize :attr:`inputs` from `inputs`. @@ -260,13 +262,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 @@ -407,32 +409,33 @@ def run(self, postflowering_stages=False): # -------------------------------- # -------------- Roots ----------- # -------------------------------- - for root_id, root_inputs in all_roots_inputs.items(): - curr_root_outputs = all_roots_outputs[root_id] - - axis_id = root_id[:2] - curr_axis_outputs = all_axes_outputs[axis_id] - - # Temperature-compensated time (delta_teq) - delta_teq = all_axes_inputs[axis_id]['delta_teq_roots'] - - # Growth - xylem_water_potential = all_axes_inputs[axis_id].get('xylem_water_potential') # Set at None if hydraulics is False - mstruct_C_growth, mstruct_growth, Nstruct_growth, Nstruct_N_growth = self.model.calculate_roots_mstruct_growth(root_inputs['sucrose'], root_inputs['amino_acids'], - root_inputs['mstruct'], delta_teq, postflowering_stages, - all_axes_inputs[axis_id]['nb_leaves'], xylem_water_potential) - # Respiration growth - curr_root_outputs['Respi_growth'] = RespirationModel.R_growth(mstruct_C_growth) - - # Update of root outputs - curr_root_outputs['mstruct'] += mstruct_growth - curr_root_outputs['AA_consumption_mstruct'] = Nstruct_N_growth - curr_root_outputs['sucrose_consumption_mstruct'] = self.model.calculate_roots_s_mstruct_sucrose(mstruct_growth, Nstruct_N_growth) - curr_root_outputs['sucrose'] -= (curr_root_outputs['sucrose_consumption_mstruct'] + curr_root_outputs['Respi_growth']) - curr_root_outputs['Nstruct'] += Nstruct_growth - curr_root_outputs['amino_acids'] -= curr_root_outputs['AA_consumption_mstruct'] - curr_root_outputs['delta_mstruct_growth'] = mstruct_growth - self.outputs['roots'][root_id] = curr_root_outputs - - # Update of axis outputs - self.outputs['axes'][axis_id] = curr_axis_outputs + if self.cnwgrass_roots: + for root_id, root_inputs in all_roots_inputs.items(): + curr_root_outputs = all_roots_outputs[root_id] + + axis_id = root_id[:2] + curr_axis_outputs = all_axes_outputs[axis_id] + + # Temperature-compensated time (delta_teq) + delta_teq = all_axes_inputs[axis_id]['delta_teq_roots'] + + # Growth + xylem_water_potential = all_axes_inputs[axis_id].get('xylem_water_potential') # Set at None if hydraulics is False + mstruct_C_growth, mstruct_growth, Nstruct_growth, Nstruct_N_growth = self.model.calculate_roots_mstruct_growth(root_inputs['sucrose'], root_inputs['amino_acids'], + root_inputs['mstruct'], delta_teq, postflowering_stages, + all_axes_inputs[axis_id]['nb_leaves'], xylem_water_potential) + # Respiration growth + curr_root_outputs['Respi_growth'] = RespirationModel.R_growth(mstruct_C_growth) + + # Update of root outputs + curr_root_outputs['mstruct'] += mstruct_growth + curr_root_outputs['AA_consumption_mstruct'] = Nstruct_N_growth + curr_root_outputs['sucrose_consumption_mstruct'] = self.model.calculate_roots_s_mstruct_sucrose(mstruct_growth, Nstruct_N_growth) + curr_root_outputs['sucrose'] -= (curr_root_outputs['sucrose_consumption_mstruct'] + curr_root_outputs['Respi_growth']) + curr_root_outputs['Nstruct'] += Nstruct_growth + curr_root_outputs['amino_acids'] -= curr_root_outputs['AA_consumption_mstruct'] + curr_root_outputs['delta_mstruct_growth'] = mstruct_growth + self.outputs['roots'][root_id] = curr_root_outputs + + # Update of axis outputs + self.outputs['axes'][axis_id] = curr_axis_outputs diff --git a/src/openalea/cnwgrass/integration/growth_facade.py b/src/openalea/cnwgrass/integration/growth_facade.py index d21267c..c2db35f 100644 --- a/src/openalea/cnwgrass/integration/growth_facade.py +++ b/src/openalea/cnwgrass/integration/growth_facade.py @@ -52,7 +52,8 @@ def __init__(self, shared_mtg, delta_t, shared_axes_inputs_outputs_df, hydraulics=False, update_parameters=None, - update_shared_df=True): + update_shared_df=True, + cnwgrass_roots=True): """ :param openalea.mtg.mtg.MTG shared_mtg: The MTG shared between all models. @@ -72,7 +73,7 @@ def __init__(self, shared_mtg, delta_t, self._shared_mtg = shared_mtg #: the MTG shared between all models - self._simulation = simulation.Simulation(delta_t=delta_t, hydraulics=hydraulics, update_parameters=update_parameters) #: the simulator to use to run the model + 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 all_growth_inputs_dict = converter.from_dataframes(model_hiddenzones_inputs_df, model_elements_inputs_df, model_roots_inputs_df, model_axes_inputs_df) From 5fa4467615eabb471c32fd2356c8a7c67e7f3520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 6 Aug 2026 17:44:45 +0200 Subject: [PATCH 07/21] conditionnal SRWC requirement --- src/openalea/cnwgrass/integration/component.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index 182b707..565cf5f 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -323,10 +323,12 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, cnmetabolism_soils_initial_state = inputs_dataframes[SOILS_INITIAL_STATE_FILENAME][ [i for i in cnmetabolism_facade.cnmetabolism_converter.SOILS_VARIABLES if i in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns]].copy() - if not hydraulics and 'SRWC' not in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns: - cnmetabolism_soils_initial_state['SRWC'] = 100 - elif hydraulics and 'SRWC' not in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns: - raise(ValueError('Hydraulics option is True but SRWC not found in {}.'.format(SOILS_INITIAL_STATE_FILENAME))) + + if cnwgrass_roots: # Not needed if there is a substitute root and soil model + if not hydraulics and 'SRWC' not in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns: + cnmetabolism_soils_initial_state['SRWC'] = 100 + elif hydraulics and 'SRWC' not in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns: + raise(ValueError('Hydraulics option is True but SRWC not found in {}.'.format(SOILS_INITIAL_STATE_FILENAME))) # Update parameters if specified if update_parameters_all_models and 'cnmetabolism' in update_parameters_all_models: @@ -590,7 +592,7 @@ def __call__(self): # run Morphogeneis Tair, Tsoil = self.meteo.loc[self.time_step_in_hours, ['air_temperature', 'soil_temperature']] - self.morphogenesis_facade_.run(Tair, Tsoil, self.Zsowing) # TODO GB why is it needed? + self.morphogenesis_facade_.run(Tair, Tsoil, self.Zsowing) # TODO GB needed for perceived temperature computation # Update geometry self.adel_wheat.update_geometry(self.g) From 1ae3cfa78c8236ba7d25fa608e2ee27c7bb4802a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Sat, 8 Aug 2026 01:21:44 +0200 Subject: [PATCH 08/21] cnwheat merged in cnmetabolism --- src/openalea/cnwgrass/cnmetabolism/model.py | 698 +++++++++--------- .../cnwgrass/cnmetabolism/simulation.py | 569 +++++++++++++- .../integration/cnmetabolism_facade.py | 128 +++- .../cnwgrass/integration/component.py | 9 +- 4 files changed, 1009 insertions(+), 395 deletions(-) diff --git a/src/openalea/cnwgrass/cnmetabolism/model.py b/src/openalea/cnwgrass/cnmetabolism/model.py index dba16c3..7117f39 100644 --- a/src/openalea/cnwgrass/cnmetabolism/model.py +++ b/src/openalea/cnwgrass/cnmetabolism/model.py @@ -87,9 +87,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 @@ -102,9 +102,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 @@ -166,7 +166,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. @@ -216,10 +216,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 @@ -276,17 +276,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 @@ -303,7 +303,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)) @@ -323,9 +323,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 @@ -339,13 +339,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) * \ @@ -355,10 +355,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) * \ @@ -369,10 +369,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 @@ -381,9 +381,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 @@ -405,7 +405,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 @@ -417,26 +417,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): @@ -451,11 +451,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 @@ -463,15 +463,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) @@ -481,15 +481,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) @@ -498,37 +498,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 """ @@ -542,28 +542,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) / @@ -576,13 +576,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 @@ -590,12 +590,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 @@ -603,10 +603,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 @@ -614,10 +614,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 @@ -636,8 +636,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 @@ -647,7 +647,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 @@ -671,7 +671,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 @@ -707,21 +707,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. @@ -733,7 +733,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 @@ -754,7 +754,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)) @@ -774,9 +774,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 @@ -786,15 +786,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 @@ -806,15 +806,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 @@ -829,13 +829,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: @@ -849,10 +849,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 @@ -861,11 +861,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 @@ -874,9 +874,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 @@ -901,35 +901,35 @@ 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 - self.Unloading_Sucrose = None #: current Unloading of sucrose from phloem to roots - self.Unloading_Amino_Acids = None #: current Unloading of amino acids from phloem to roots + self.Unloading_Sucrose = 0. #: current Unloading of sucrose from phloem to roots + self.Unloading_Amino_Acids = 0. #: current Unloading of amino acids from phloem to roots # other fluxes - self.Export_Nitrates = None #: Total export of nitrates from roots to shoot organs integrated over a delta t (µmol` N) - self.Export_Amino_Acids = None #: 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 = None #: Total export of cytokinin from roots to shoot organs integrated over a delta t (AU) + 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) @@ -941,10 +941,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 @@ -964,26 +964,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))) @@ -996,11 +996,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 @@ -1012,11 +1012,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 @@ -1027,30 +1027,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 @@ -1061,19 +1061,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))) * @@ -1084,51 +1084,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: @@ -1140,8 +1140,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) @@ -1177,12 +1177,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 @@ -1192,11 +1192,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 @@ -1206,12 +1206,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 @@ -1256,7 +1256,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 @@ -1343,9 +1343,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 @@ -1357,48 +1357,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): @@ -1413,12 +1413,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 @@ -1437,11 +1437,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 """ if Loading_Sucrose <= 0: @@ -1458,11 +1458,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 @@ -1470,13 +1470,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 """ if triosesP <= 0: @@ -1487,25 +1487,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 """ if triosesP <= 0: @@ -1516,44 +1516,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 @@ -1561,28 +1561,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) / @@ -1592,14 +1592,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: @@ -1610,14 +1610,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: @@ -1627,14 +1627,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 """ if nitrates <= 0 or triosesP <= 0: @@ -1647,13 +1647,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) / @@ -1662,14 +1662,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) @@ -1682,43 +1682,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 @@ -1761,12 +1761,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 @@ -1776,10 +1776,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) @@ -1787,14 +1787,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 @@ -1802,10 +1802,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) @@ -1813,10 +1813,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 @@ -1825,13 +1825,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) @@ -1839,10 +1839,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) @@ -1914,8 +1914,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 (%) """ @@ -1929,15 +1929,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 @@ -1958,9 +1958,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 @@ -1975,9 +1975,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)) @@ -1989,7 +1989,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 @@ -2000,12 +2000,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 7fc341b..b651783 100644 --- a/src/openalea/cnwgrass/cnmetabolism/simulation.py +++ b/src/openalea/cnwgrass/cnmetabolism/simulation.py @@ -61,17 +61,17 @@ class Simulation(object): This model must define a class implementing these functions: * R_Nnit_upt(U_Nnit, sucrose): Nitrate uptake respiration. * Parameters: - - `U_Nnit` (:class:`float`) - uptake of N nitrates (µmol` N) - - `sucrose` (:class:`float`) - amount of C sucrose in organ (µmol` C) - * Returns: _R_Nnit_upt (µmol` C respired) + - `U_Nnit` (:class:`float`) - uptake of N nitrates (�mol` N) + - `sucrose` (:class:`float`) - amount of C sucrose in organ (�mol` C) + * Returns: _R_Nnit_upt (�mol` C respired) * Returns Type: :class:`float` * R_phloem(sucrose_loading, sucrose, mstruct): Phloem loading respiration * Parameters: - - `sucrose_loading` (:class:`float`) - Loading flux from the C substrate pool to phloem (µmol` C g-1 mstruct) - - `sucrose` (:class:`float`) - amount of C sucrose in organ (µmol` C) + - `sucrose_loading` (:class:`float`) - Loading flux from the C substrate pool to phloem (�mol` C g-1 mstruct) + - `sucrose` (:class:`float`) - amount of C sucrose in organ (�mol` C) - `mstruct` (:class:`float`) - structural dry mass of organ (g) - * Returns: _R_phloem (µmol` C respired) + * Returns: _R_phloem (�mol` C respired) * Returns Type: :class:`float` * R_Nnit_red(s_amino_acids, sucrose, mstruct, root=False): Nitrate reduction-linked respiration @@ -79,30 +79,30 @@ class Simulation(object): and reducing power obtained directly from photosynthesis (rather than C substrate) * Parameters: - - `s_amino_acids` (:class:`float`) - consumption of N for the synthesis of amino acids (µmol` N g-1 mstruct) + - `s_amino_acids` (:class:`float`) - consumption of N for the synthesis of amino acids (�mol` N g-1 mstruct) (in the present version, this is used to approximate nitrate reduction needed in the original model of Thornley and Cannell, 2000) - - `sucrose` (:class:`float`) - amount of C sucrose in organ (µmol` C) + - `sucrose` (:class:`float`) - amount of C sucrose in organ (�mol` C) - `mstruct` (:class:`float`) - structural dry mass of organ (g) - `root` (:class:`bool`) - specifies if the nitrate reduction-linked respiration is computed for shoot (False) or root (True) tissues. - * Returns: _R_Nnit_upt (µmol` C respired) + * Returns: _R_Nnit_upt (�mol` C respired) * Returns Type: :class:`float` * R_residual(sucrose, mstruct, Ntot, delta_t, Ts): Residual maintenance respiration (cost from protein turn-over, cell ion gradients, futile cycles...) * Parameters: - - `sucrose` (:class:`float`) - amount of C sucrose (µmol` C) + - `sucrose` (:class:`float`) - amount of C sucrose (�mol` C) - `mstruct` (:class:`float`) - structural dry mass of organ (g) - - `Ntot` (:class:`float`) - total N in organ (µmol` N) + - `Ntot` (:class:`float`) - total N in organ (�mol` N) - `delta_t` (:class:`float`) - timestep (s) - - `Ts` (:class:`float`) - organ temperature (°C) - * Returns: _R_residual (µmol` C respired) + - `Ts` (:class:`float`) - organ temperature (�C) + * Returns: _R_residual (�mol` C respired) * Returns Type: :class:`float` * R_grain_growth(mstruct_growth, starch_filling, mstruct): Grain growth respiration * Parameters: - - `mstruct_growth` (:class:`float`) - gross growth of grain structure (µmol` C added in grain structure) - - `starch_filling` (:class:`float`) - gross growth of grain starch (µmol` C added in grain starch g-1 mstruct) + - `mstruct_growth` (:class:`float`) - gross growth of grain structure (�mol` C added in grain structure) + - `starch_filling` (:class:`float`) - gross growth of grain starch (�mol` C added in grain starch g-1 mstruct) - `mstruct` (:class:`float`) - structural dry mass of organ (g) - * Returns: R_grain_growth (µmol` C respired) + * Returns: R_grain_growth (�mol` C respired) * Returns Type: :class:`float` :param int delta_t: the delta t of the simulation (in seconds) ; default is `1`. @@ -233,7 +233,7 @@ class Simulation(object): ORGANS_FLUXES = ['Export_Amino_Acids', 'Export_Nitrates', 'Export_cytokinins', 'S_Amino_Acids', 'S_cytokinins', 'S_grain_starch', 'S_grain_structure', 'S_Proteins', 'Unloading_Amino_Acids', 'Unloading_Sucrose', 'Uptake_Nitrates', 'D_starch', 'D_proteins'] #: the variables computed by integrating values of organ components parameters/variables recursively - ORGANS_INTEGRATIVE_VARIABLES = ['Total_Organic_Nitrogen'] + ORGANS_INTEGRATIVE_VARIABLES = ['Total_Organic_Nitrogen', 'Unloading_Sucrose_shoot_organs', 'Unloading_Amino_Acids_shoot_organs'] #: all the variables computed during a run step of the simulation at organ scale ORGANS_RUN_VARIABLES = ORGANS_STATE + ORGANS_INTERMEDIATE_VARIABLES + ORGANS_FLUXES + ORGANS_INTEGRATIVE_VARIABLES @@ -336,7 +336,7 @@ class Simulation(object): model.PhotosyntheticOrganElement: 'cnmetabolism.derivatives.elements', model.Soil: 'cnmetabolism.derivatives.soils'}} - def __init__(self, respiration_model, delta_t=1, culm_density=None, interpolate_forcing=False, senescence_forcing_delta_t=None, photosynthesis_forcing_delta_t=None, external_soil_model=False): + def __init__(self, respiration_model, delta_t=1, culm_density=None, interpolate_forcing=False, senescence_forcing_delta_t=None, photosynthesis_forcing_delta_t=None, external_soil_model=False, isolated_roots=False, cnwgrass_roots=True): self.respiration_model = respiration_model #: the model of respiration to use @@ -351,6 +351,12 @@ def __init__(self, respiration_model, delta_t=1, culm_density=None, interpolate_ self.initial_conditions = [] #: the initial conditions of the compartments in the population and soils self.initial_conditions_mapping = {} #: dictionary to map the compartments to their indexes in :attr:`initial_conditions` + if isolated_roots: + self.initial_conditions_roots = [] + self.initial_conditions_mapping_roots = {} + if not cnwgrass_roots: + self.first_initialization = True + self.delta_t = delta_t #: the delta t of the simulation (in seconds) self.time_step = self.delta_t / 3600.0 #: time step of the simulation (in hours) @@ -363,6 +369,9 @@ def __init__(self, respiration_model, delta_t=1, culm_density=None, interpolate_ self.external_soil_model = external_soil_model #: a boolean flag which indicates if an external soil model is coupled to cnmetabolism. + self.isolated_roots = isolated_roots + self.cnwgrass_roots = cnwgrass_roots + # set the loggers for compartments and derivatives compartments_logger = logging.getLogger('cnmetabolism.compartments') derivatives_logger = logging.getLogger('cnmetabolism.derivatives') @@ -447,7 +456,7 @@ def initialize(self, population, soils, Tsoil=12): :param model.Population population: a population of plants. :param dict soils: the soil associated to each axis. `soils` must be a dictionary with the same structure as :attr:`soils` - :param float Tsoil: soil temperature (°C) + :param float Tsoil: soil temperature (�C) """ logger = logging.getLogger(__name__) @@ -459,6 +468,9 @@ def initialize(self, population, soils, Tsoil=12): self.soils.clear() del self.initial_conditions[:] self.initial_conditions_mapping.clear() + if self.isolated_roots: + del self.initial_conditions_roots[:] + self.initial_conditions_mapping_roots.clear() # create new population and soils self.population.plants.extend(population.plants) @@ -577,10 +589,27 @@ def _init_initial_conditions(model_object, index): index += 1 return index + # initialize initial conditions roots + def _init_initial_conditions_roots(root_object, index): + compartments_names = Simulation.MODEL_COMPARTMENTS_NAMES[model.Organ] + self.initial_conditions_mapping_roots[root_object] = {} + for compartment_name in compartments_names: + if hasattr(root_object, compartment_name): + self.initial_conditions_mapping_roots[root_object][compartment_name] = index + self.initial_conditions_roots.append(0) + index += 1 + return index + i = 0 + i_root = 0 - for soil in self.soils.values(): - i = _init_initial_conditions(soil, i) + # We initialize soil only if roots are present + if self.cnwgrass_roots: + for soil in soils.values(): + if not self.isolated_roots: + i = _init_initial_conditions(soil, i) + else: + i_root = _init_initial_conditions_roots(soil, i_root) for plant in self.population.plants: i = _init_initial_conditions(plant, i) @@ -589,7 +618,17 @@ def _init_initial_conditions(model_object, index): for organ in (axis.roots, axis.phloem, axis.grains, axis.endosperm): if organ is None: continue - i = _init_initial_conditions(organ, i) + 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: + i_root = _init_initial_conditions_roots(organ, i_root) + self.first_initialization = False + + 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): @@ -620,26 +659,42 @@ def run(self): self._interpolate_forcing() self._update_initial_conditions() + if self.isolated_roots and self.cnwgrass_roots: + self._update_initial_conditions_roots() 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` - 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) + 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) - self.nfev_total += sol.nfev + self.nfev_total += sol.nfev + # 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) + + elif self.cnwgrass_roots: + sol_shoot = solve_ivp(fun=self._calculate_shoot_derivatives, t_span=self.time_grid, y0=self.initial_conditions, + method='BDF', t_eval=None, dense_output=False) + 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) + + else: + sol_shoot = 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, + ) + if logger.isEnabledFor(logging.DEBUG): logger.debug("Run of the solver DONE") - # check the integration ; raise an exception if the integration failed - if not sol.success: - message = "Integration failed: {}".format(sol.message) - logger.exception(message) - raise SimulationRunError(message) - # Re-compute integrative variables self.population.calculate_aggregated_variables() @@ -656,6 +711,14 @@ def _update_initial_conditions(self): for compartment_name, compartment_index in compartments.items(): self.initial_conditions[compartment_index] = getattr(model_object, compartment_name) + def _update_initial_conditions_roots(self): + """Update the compartments values in :attr:`initial_conditions` from the compartments values of :attr:`population` and :attr:`soils`. + """ + # Update the compartments values + for model_object, compartments in self.initial_conditions_mapping_roots.items(): + for compartment_name, compartment_index in compartments.items(): + self.initial_conditions_roots[compartment_index] = getattr(model_object, compartment_name) + def _interpolate_forcing(self): """Create functions to interpolate the forcing of the model to any time inside the time grid (see `self.time_grid`). @@ -759,14 +822,12 @@ def update_rows(model_object, indexes, rows, i): if organ is None: continue if organ is phytomer.hiddenzone: - i = update_rows(organ, [t, plant.index, axis.label, phytomer.index], all_rows[ - model.HiddenZone], i) + i = update_rows(organ, [t, plant.index, axis.label, phytomer.index], all_rows[model.HiddenZone], i) continue for element in (organ.exposed_element, organ.enclosed_element): if element is None: continue - i = update_rows(element, [t, plant.index, axis.label, phytomer.index, organ.label, element.label], all_rows[ - model.PhotosyntheticOrganElement], i) + i = update_rows(element, [t, plant.index, axis.label, phytomer.index, organ.label, element.label], all_rows[model.PhotosyntheticOrganElement], i) row_sep = '\n' column_sep = ',' @@ -1114,4 +1175,442 @@ def _calculate_all_derivatives(self, t, y): derivatives_logger = logging.getLogger('cnmetabolism.derivatives') if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): self._log_compartments(t_abs, y_derivatives, Simulation.LOGGERS_NAMES['derivatives']) + + return y_derivatives + + + + def _calculate_shoot_derivatives(self, t, y): + """Compute the derivative of `y` at `t`. + + :meth:`_calculate_all_derivatives` is passed as **func** argument to + :func:`solve_ivp(fun, t_span, y0,...) `. + :meth:`_calculate_all_derivatives` is called automatically by + :func:`scipy.integrate.solve_ivp `. + + First call to :meth:`_calculate_all_derivatives` uses `y` = **y0** and + `t` = **t_span** [0], where **y0** and **t_span** are arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + + Following calls to :meth:`_calculate_all_derivatives` use `t` in [**t_span** [0], **t_span** [1]]. + + :param float t: The current t at which we want to compute the derivatives. + Values of `t` are chosen automatically by :func:`scipy.integrate.solve_ivp`. + At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp`, + `t` = **t_span** [0], where **t_span** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + For each following call to :meth:`_calculate_all_derivatives`, `t` belongs + to the interval [**t_span** [0], **t_span** [1]]. + :param list [float] y: The current values of y. + At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp`, `y` = **y0** + where **y0** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + Then, values of `y` are chosen automatically by :func:`scipy.integrate.solve_ivp`. + + :return: The derivatives of `y` at `t`. + :rtype: list [float] + """ + logger = logging.getLogger(__name__) + + if logger.isEnabledFor(logging.DEBUG): + t_abs = t + self.t_offset + logger.debug('t = {}'.format(t_abs)) + + if self.interpolate_forcing: + # Update state parameters using interpolation functions + for plant in self.population.plants: + for axis in plant.axes: + if axis.roots is not None: + roots_id = (plant.index, axis.label) + for forcing_label in Simulation.ROOTS_forcing: + setattr(axis.roots, forcing_label, float(self.interpolation_functions[roots_id][forcing_label](t))) + for phytomer in axis.phytomers: + for organ in (phytomer.lamina, phytomer.sheath): + if organ is None: + continue + for element in (organ.exposed_element, organ.enclosed_element): + if element is not None: + element_id = (plant.index, axis.label, phytomer.index, organ.label, element.label) + for forcing_label in Simulation.ELEMENTS_forcing: + setattr(element, forcing_label, float(self.interpolation_functions[element_id][forcing_label](t))) + + # Compute integrative variables + self.population.calculate_aggregated_variables() + + compartments_logger = logging.getLogger('cnmetabolism.compartments') + if logger.isEnabledFor(logging.DEBUG) and compartments_logger.isEnabledFor(logging.DEBUG): + self._log_compartments(t_abs, y, Simulation.LOGGERS_NAMES['compartments']) + + # check that the solver is not crashed + y_isnan = np.isnan(y) + if y_isnan.any(): + message = 'The solver did not manage to compute a compartment. See the logs. NaN found in y' + logger.exception(message) + raise SimulationRunError(message) + + y_derivatives = np.zeros_like(y) + + for plant in self.population.plants: + 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) + + # 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 + 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): + if organ is not None: + for element in (organ.exposed_element, organ.enclosed_element): + 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) + + # Compute the regulating factor of root exports by shoot transpiration + axis.roots.regul_transpiration = axis.roots.calculate_regul_transpiration(axis.Total_Transpiration) + + # compute the derivative of each photosynthetic organ element compartment + for phytomer in axis.phytomers: + # Hidden zone + hiddenzone = phytomer.hiddenzone + if phytomer.hiddenzone is not None: + hiddenzone.sucrose = y[hiddenzone._i_sucrose] + 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 + + for organ in (phytomer.chaff, phytomer.peduncle, phytomer.lamina, phytomer.internode, phytomer.sheath): + if organ is None: + continue + + for element in (organ.exposed_element, organ.enclosed_element): + if element is None or element.green_area <= 0.25E-6 or element.mstruct <= 0.0: + continue + + element.T_effect_conductivity = plant.calculate_temperature_effect_on_conductivity(element.Ts) + element.T_effect_Vmax = plant.calculate_temperature_effect_on_Vmax(element.Ts) + + # Unpack here to avoid doubling the element lookup cost + i_starch, i_sucrose, i_triosesP, i_fructan, i_nitrates, i_amino_acids, i_proteins, i_cytokinins = element._i_starch, element._i_sucrose, element._i_triosesP, element._i_fructan, element._i_nitrates, element._i_amino_acids, element._i_proteins, element._i_cytokinins + + element.starch = y[i_starch] + element.sucrose = y[i_sucrose] + element.triosesP = y[i_triosesP] + element.fructan = y[i_fructan] + element.nitrates = y[i_nitrates] + element.amino_acids = y[i_amino_acids] + element.proteins = y[i_proteins] + element.cytokinins = y[i_cytokinins] + + # intermediate variables + element.Photosynthesis = element.calculate_total_Photosynthesis(element.Ag, element.green_area) + + # flows + if element.is_growing and phytomer.hiddenzone is not None: #: Export of sucrose and amino acids towards the HZ. Several growing elements might export toward the HZ at the same time (leaf and internode) + element.Loading_Sucrose = element.calculate_export_sucrose(element.sucrose, hiddenzone.sucrose, hiddenzone.mstruct, element.T_effect_conductivity) + hiddenzone_Loading_Sucrose_contribution += element.Loading_Sucrose + element.Loading_Amino_Acids = element.calculate_Export_Amino_Acids(element.amino_acids, hiddenzone.amino_acids, hiddenzone.mstruct, element.T_effect_conductivity) + 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.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) + element.D_Fructan = element.calculate_D_Fructan(element.sucrose, element.fructan, element.T_effect_Vmax) + element.S_Starch = element.calculate_S_Starch(element.triosesP, element.T_effect_Vmax) + element.D_Starch = element.calculate_D_Starch(element.starch, element.T_effect_Vmax) + 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.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.D_cytokinins = element.calculate_D_cytokinins(element.cytokinins, element.T_effect_Vmax) + + # compartments derivatives + starch_derivative = element.calculate_starch_derivative(element.S_Starch, element.D_Starch) + element.R_residual = self.respiration_model.RespirationModel.R_residual(element.sucrose, element.mstruct * element.__class__.PARAMETERS.ALPHA, + element.Total_Organic_Nitrogen, element.Ts) + element_sum_respi = element.R_phloem_loading + element.R_Nnit_red + element.R_residual + sucrose_derivative = element.calculate_sucrose_derivative(element.S_Sucrose, element.D_Starch, element.Loading_Sucrose, element.S_Fructan, + element.D_Fructan, element_sum_respi) + triosesP_derivative = element.calculate_triosesP_derivative(element.Photosynthesis, element.S_Sucrose, element.S_Starch, element.S_Amino_Acids) + fructan_derivative = element.calculate_fructan_derivative(element.S_Fructan, element.D_Fructan) + nitrates_derivative = element.calculate_nitrates_derivative(element.Nitrates_import, element.S_Amino_Acids) + amino_acids_derivative = element.calculate_amino_acids_derivative(element.Amino_Acids_import, element.S_Amino_Acids, element.S_Proteins, element.D_Proteins, + element.Loading_Amino_Acids) + proteins_derivative = element.calculate_proteins_derivative(element.S_Proteins, element.D_Proteins) + cytokinins_derivative = element.calculate_cytokinins_derivative(element.cytokinins_import, element.D_cytokinins, phytomer.index, element.cytokinins) + + y_derivatives[i_starch] = starch_derivative + y_derivatives[i_sucrose] = sucrose_derivative + y_derivatives[i_triosesP] = triosesP_derivative + y_derivatives[i_fructan] = fructan_derivative + y_derivatives[i_nitrates] = nitrates_derivative + y_derivatives[i_amino_acids] = amino_acids_derivative + y_derivatives[i_proteins] = proteins_derivative + y_derivatives[i_cytokinins] = cytokinins_derivative + + 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) + + # 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) + + # 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) + + # Fructan degradation + hiddenzone.D_Fructan = hiddenzone.calculate_D_Fructan(hiddenzone.sucrose, hiddenzone.fructan, axis.T_effect_Vmax) + + # Synthesis proteins + hiddenzone.S_Proteins = hiddenzone.calculate_S_proteins(hiddenzone.amino_acids, axis.T_effect_Vmax) + + # Degradation proteins + hiddenzone.D_Proteins = hiddenzone.calculate_D_Proteins(hiddenzone.proteins, 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) + + # compute the derivatives of the hidden zone + y_derivatives[hiddenzone._i_sucrose] = hiddenzone.calculate_sucrose_derivative(hiddenzone.Unloading_Sucrose, hiddenzone.S_Fructan, + hiddenzone.D_Fructan, hiddenzone_Loading_Sucrose_contribution, + hiddenzone.R_residual) + y_derivatives[hiddenzone._i_amino_acids] = hiddenzone.calculate_amino_acids_derivative(hiddenzone.Unloading_Amino_Acids, hiddenzone.S_Proteins, + hiddenzone.D_Proteins, + hiddenzone_Loading_Amino_Acids_contribution) + 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 + + 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) + + # 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 + + derivatives_logger = logging.getLogger('cnmetabolism.derivatives') + if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): + self._log_compartments(t_abs, y_derivatives, Simulation.LOGGERS_NAMES['derivatives']) + return y_derivatives + + def _calculate_root_derivatives(self, t, y): + """Compute the derivative of `y` at `t`. + + :meth:`_calculate_all_derivatives` is passed as **func** argument to + :func:`solve_ivp(fun, t_span, y0,...) `. + :meth:`_calculate_all_derivatives` is called automatically by + :func:`scipy.integrate.solve_ivp `. + + First call to :meth:`_calculate_all_derivatives` uses `y` = **y0** and + `t` = **t_span** [0], where **y0** and **t_span** are arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + + Following calls to :meth:`_calculate_all_derivatives` use `t` in [**t_span** [0], **t_span** [1]]. + + :param float t: The current t at which we want to compute the derivatives. + Values of `t` are chosen automatically by :func:`scipy.integrate.solve_ivp`. + At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp`, + `t` = **t_span** [0], where **t_span** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + For each following call to :meth:`_calculate_all_derivatives`, `t` belongs + to the interval [**t_span** [0], **t_span** [1]]. + :param list [float] y: The current values of y. + At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp`, `y` = **y0** + where **y0** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + Then, values of `y` are chosen automatically by :func:`scipy.integrate.solve_ivp`. + + :return: The derivatives of `y` at `t`. + :rtype: list [float] + """ + logger = logging.getLogger(__name__) + + if logger.isEnabledFor(logging.DEBUG): + t_abs = t + self.t_offset + logger.debug('t = {}'.format(t_abs)) + + if self.interpolate_forcing: + # Update state parameters using interpolation functions + for plant in self.population.plants: + for axis in plant.axes: + if axis.roots is not None: + roots_id = (plant.index, axis.label) + for forcing_label in Simulation.ROOTS_forcing: + setattr(axis.roots, forcing_label, float(self.interpolation_functions[roots_id][forcing_label](t))) + for phytomer in axis.phytomers: + for organ in (phytomer.lamina, phytomer.sheath): + if organ is None: + continue + for element in (organ.exposed_element, organ.enclosed_element): + if element is not None: + element_id = (plant.index, axis.label, phytomer.index, organ.label, element.label) + for forcing_label in Simulation.ELEMENTS_forcing: + setattr(element, forcing_label, float(self.interpolation_functions[element_id][forcing_label](t))) + + # Compute integrative variables + self.population.calculate_aggregated_variables() + + compartments_logger = logging.getLogger('cnmetabolism.compartments') + if logger.isEnabledFor(logging.DEBUG) and compartments_logger.isEnabledFor(logging.DEBUG): + self._log_compartments(t_abs, y, Simulation.LOGGERS_NAMES['compartments']) + + # check that the solver is not crashed + y_isnan = np.isnan(y) + if y_isnan.any(): + message = 'The solver did not manage to compute a compartment. See the logs. NaN found in y' + logger.exception(message) + raise SimulationRunError(message) + + y_derivatives = np.zeros_like(y) + # TODO: TEMP !!!! + soil = self.soils[(1, 'MS')] + soil.T_effect_Vmax = soil.calculate_temperature_effect_on_Vmax(soil.Tsoil) + if not self.external_soil_model: + soil_contributors = [] + soil.nitrates = y[self.initial_conditions_mapping_roots[soil]['nitrates']] + soil.Conc_Nitrates_Soil = soil.calculate_Conc_Nitrates(soil.nitrates) + soil.T_effect_conductivity = soil.calculate_temperature_effect_on_conductivity(soil.Tsoil) + + for plant in self.population.plants: + 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) + + # 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']] + axis.roots.sucrose = y[self.initial_conditions_mapping_roots[axis.roots]['sucrose']] + axis.roots.cytokinins = y[self.initial_conditions_mapping_roots[axis.roots]['cytokinins']] + + # 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) + + # compute the derivative of each compartment of roots + # flows + 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_roots[axis.roots]['sucrose']] = sucrose_derivative + y_derivatives[self.initial_conditions_mapping_roots[axis.roots]['nitrates']] = nitrates_derivative + y_derivatives[self.initial_conditions_mapping_roots[axis.roots]['amino_acids']] = amino_acids_derivative + y_derivatives[self.initial_conditions_mapping_roots[axis.roots]['cytokinins']] = cytokinins_derivative + + if not self.external_soil_model: + # compute the derivative of each compartment of soil + soil.mineralisation = soil.calculate_mineralisation(soil.T_effect_Vmax) + y_derivatives[self.initial_conditions_mapping_roots[soil]['nitrates']] = soil.calculate_nitrates_derivative(soil.mineralisation, soil_contributors, self.culm_density, soil.constant_Conc_Nitrates) + + derivatives_logger = logging.getLogger('cnmetabolism.derivatives') + if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): + self._log_compartments(t_abs, y_derivatives, Simulation.LOGGERS_NAMES['derivatives']) + + return y_derivatives + + diff --git a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py index e112116..ca25e51 100644 --- a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py +++ b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py @@ -80,7 +80,9 @@ def __init__(self, shared_mtg, delta_t, culm_density, update_parameters, shared_soils_inputs_outputs_df, tillers_replications=None, external_soil_model=False, - update_shared_df=True): + update_shared_df=True, + isolated_roots=False, + cnwgrass_roots=True): """ :param openalea.mtg.mtg.MTG shared_mtg: The MTG shared between all models. :param int delta_t: The delta between two runs, in seconds. @@ -106,7 +108,7 @@ def __init__(self, shared_mtg, delta_t, culm_density, update_parameters, self.tillers_replications = tillers_replications 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) + 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) self.population, self.soils = cnmetabolism_converter.from_dataframes(model_axes_inputs_df, model_organs_inputs_df, model_hiddenzones_inputs_df, model_elements_inputs_df, model_soils_inputs_df) @@ -128,17 +130,20 @@ def __init__(self, shared_mtg, delta_t, culm_density, update_parameters, cnmetabolism_elements_data_df=model_elements_inputs_df, cnmetabolism_soils_data_df=model_soils_inputs_df) + self.isolated_roots = isolated_roots + 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. """ self._initialize_model(Tair=Tair, Tsoil=Tsoil) + self._initialize_indices_and_sparcity() self._simulation.run() self._update_shared_MTG() @@ -219,8 +224,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 @@ -269,7 +274,10 @@ def _initialize_model(self, Tair, Tsoil): 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] - cnmetabolism_organ_data_names = set(cnmetabolism_simulation.Simulation.ORGANS_STATE).intersection(cnmetabolism_organ.__dict__) + 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: @@ -417,6 +425,112 @@ def _initialize_model(self, Tair, Tsoil): self._simulation.initialize(self.population, self.soils, Tsoil=Tsoil) + def _initialize_indices_and_sparcity(self): + """ + TODO + """ + n = len(self._simulation.initial_conditions) + S = lil_matrix((n, n), dtype=bool) + # diagonal always present + S.setdiag(True) + + icm = self._simulation.initial_conditions_mapping + for plant in self._simulation.population.plants: + for axis in plant.axes: + axis._phloem_contributors = [axis.roots] + for variable in ('sucrose', 'amino_acids'): + setattr(axis.phloem, f"_i_{variable}", icm[axis.phloem][variable]) + + # Phloem indices + i_ph_suc = axis.phloem._i_sucrose + i_ph_aa = axis.phloem._i_amino_acids + + # phloem sucrose row depends on phloem sucrose (diag already) and MAY depend on phloem AA via unloading logic, over-approx for safety, could be removed + S[i_ph_suc, i_ph_aa] = True + S[i_ph_aa, i_ph_suc] = True + + # Collect per-axis element/HZ columns we must connect + E_suc_cols, E_aa_cols = [], [] + HZ_suc_cols, HZ_aa_cols = [], [] + + for variable in ('moistening', 'starch', 'proteins'): + setattr(axis.endosperm, f"_i_{variable}", icm[axis.phloem][variable]) + + # TODO include endosperm in jacobian sparcity matrix + + for phytomer in axis.phytomers: + hiddenzone = phytomer.hiddenzone + if hiddenzone is not None: + if hiddenzone.mstruct > 0: + hz_idxs = [] + for variable in ('sucrose', 'fructan', 'amino_acids', 'proteins'): + setattr(hiddenzone, f"_i_{variable}", icm[hiddenzone][variable]) + hz_idxs.append(icm[hiddenzone][variable]) + for i in hz_idxs: + S.rows[i].extend(hz_idxs); S.data[i].extend([True]*len(hz_idxs)) + # hidden zone depends on phloem unloading + S[i, i_ph_suc] = True; S[i, i_ph_aa] = True + HZ_suc_cols.append(icm[hiddenzone]['sucrose']) + HZ_aa_cols.append(icm[hiddenzone]['amino_acids']) + + axis._phloem_contributors.append(hiddenzone) + + for organ in (phytomer.chaff, phytomer.peduncle, phytomer.lamina, phytomer.internode, phytomer.sheath): + if organ is not None: + for element in (organ.exposed_element, organ.enclosed_element): + if element is not None and element.green_area > 0.25E-6 and element.mstruct > 0.0: # Ignored here as element could change status during solver iteration + # if element is not None: + elt_idxs = [] + for variable in ('starch', 'sucrose', 'triosesP', 'fructan', 'nitrates', 'amino_acids', 'proteins', 'cytokinins'): + setattr(element, f"_i_{variable}", icm[element][variable]) + elt_idxs.append(icm[element][variable]) + for i in elt_idxs: + S.rows[i].extend(elt_idxs); S.data[i].extend([True]*len(elt_idxs)) + # element loads/unloads vs phloem + S[i, i_ph_suc] = True; S[i, i_ph_aa] = True + + i_e_suc = icm[element]['sucrose'] + i_e_aa = icm[element]['amino_acids'] + # Since roots are not computed by CN-Wheat here, nitrates and cytokinins act as constants + # i_e_nit = icm[element]['nitrates'] + # i_e_cyt = icm[element]['cytokinins'] + E_suc_cols.append(i_e_suc) + E_aa_cols.append(i_e_aa) + + # Element rows depend on HZ pools (growing/export case) + if hiddenzone is not None: + S[i_e_suc, icm[hiddenzone]['sucrose']] = True + S[i_e_aa, icm[hiddenzone]['amino_acids']] = True + + if not element.is_growing: + 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 + for i in HZ_suc_cols: + S.rows[i].extend(E_suc_cols); S.data[i].extend([True]*len(E_suc_cols)) + for i in HZ_aa_cols: + S.rows[i].extend(E_aa_cols); S.data[i].extend([True]*len(E_aa_cols)) + + # b) Phloem rows depend on HZ unloading AND element loading + S.rows[i_ph_suc].extend(E_suc_cols + HZ_suc_cols); S.data[i_ph_suc].extend([True]*(len(E_suc_cols) + len(HZ_suc_cols))) + 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: + grain_idxs = [] + for variable in ('structure','starch','proteins','age_from_flowering'): + setattr(axis.grains, f"_i_{variable}", icm[axis.grains][variable]) + grain_idxs.append(icm[axis.grains][variable]) + 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 + + self._simulation._jac_sparsity_shoot = S.tocsr() + + def _update_shared_MTG(self): """ Update the MTG shared between all models from the population of CN-Metabolism. diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index 565cf5f..bfd5e06 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, heterogeneous_canopy=True, show_3Dplant = False, + tillers_replications=None, 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): @@ -155,6 +155,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.show_3Dplant = show_3Dplant self.heterogeneous_canopy = heterogeneous_canopy self.computing_light_interception = computing_light_interception + self.external_soil_model = external_soil_model self.hydraulics = hydraulics self.option_static = option_static self.cnwgrass_roots = cnwgrass_roots @@ -411,7 +412,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, i in inputs_dataframes[SOILS_INITIAL_STATE_FILENAME].columns]].copy() # Facade initialisation - self.hydraulics_facade_ = hydraulics_facade.hydraulicsFacade(g, + self.hydraulics_facade_ = hydraulics_facade.hydraulicsFacade(self.g, HYDRAULICS_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR, update_parameters_hydraulics, hydraulics_axes_initial_state, @@ -592,7 +593,7 @@ def __call__(self): # run Morphogeneis Tair, Tsoil = self.meteo.loc[self.time_step_in_hours, ['air_temperature', 'soil_temperature']] - self.morphogenesis_facade_.run(Tair, Tsoil, self.Zsowing) # TODO GB needed for perceived temperature computation + self.morphogenesis_facade_.run(Tair, Tsoil, self.Zsowing) # NOTE Zsowing needed for perceived temperature computation # Update geometry self.adel_wheat.update_geometry(self.g) @@ -646,7 +647,7 @@ def __call__(self): self.cnmetabolism_facade_.soils[(1, 'MS')].nitrates += self.N_fertilizations[self.time_step_in_hours] # Force root nitrate uptake if specified - if external_soil_model and step_callback is not None: + if self.external_soil_model and step_callback is not None: try: step_callback['nitrate_uptake'](self.time_step_in_hours, cnmetabolism_facade_.population, g) except KeyError: From e0512f3b50b3e261bef502d309e9c3e7b70afa86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Sun, 9 Aug 2026 18:04:45 +0200 Subject: [PATCH 09/21] prior hydraulic model edition and couplign --- .../cnwgrass/cnmetabolism/simulation.py | 20 +++--- .../cnwgrass/hydraulics/simulation.py | 70 +++++++++++++++++-- .../integration/cnmetabolism_facade.py | 8 ++- .../cnwgrass/integration/component.py | 6 +- .../cnwgrass/integration/hydraulics_facade.py | 6 +- 5 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/openalea/cnwgrass/cnmetabolism/simulation.py b/src/openalea/cnwgrass/cnmetabolism/simulation.py index b651783..bc2eac7 100644 --- a/src/openalea/cnwgrass/cnmetabolism/simulation.py +++ b/src/openalea/cnwgrass/cnmetabolism/simulation.py @@ -671,29 +671,29 @@ def run(self): 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) - self.nfev_total += sol.nfev - - # 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) - elif self.cnwgrass_roots: - sol_shoot = solve_ivp(fun=self._calculate_shoot_derivatives, t_span=self.time_grid, y0=self.initial_conditions, + 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) 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) else: - sol_shoot = solve_ivp(fun=self._calculate_shoot_derivatives, t_span=self.time_grid, + 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, ) + self.nfev_total += sol.nfev + if logger.isEnabledFor(logging.DEBUG): logger.debug("Run of the solver DONE") + + # check the integration ; raise an exception if the integration failed + if not sol.success: + message = "Integration failed: {}".format(sol.message) + logger.exception(message) + raise SimulationRunError(message) # Re-compute integrative variables self.population.calculate_aggregated_variables() diff --git a/src/openalea/cnwgrass/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index ef0912f..455bf0e 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -249,7 +249,7 @@ 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): + 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): self.population = model.Population() #: the population to simulate on #: The inputs of the soils. @@ -261,6 +261,12 @@ def __init__(self, delta_t=1, interpolate_forcing=False, elements_forcing_delta_ self.initial_conditions = [] #: the initial conditions of the compartments in the population and soil self.initial_conditions_mapping = {} #: dictionary to map the compartments to their indexes in :attr:`initial_conditions` + if isolated_roots: + self.initial_conditions_roots = [] + self.initial_conditions_mapping_roots = {} + if not cnwgrass_roots: + self.first_initialization = True + self.delta_t = delta_t #: the delta t of the simulation (in seconds) self.time_step = self.delta_t / 3600.0 #: time step of the simulation (in hours) @@ -269,6 +275,9 @@ def __init__(self, delta_t=1, interpolate_forcing=False, elements_forcing_delta_ self.interpolate_forcing = interpolate_forcing #: a boolean flag which indicates if we want to interpolate or not the forcing (True: interpolate, False: do not interpolate) + self.isolated_roots = isolated_roots + self.cnwgrass_roots = cnwgrass_roots + # set the loggers for compartments and derivatives compartments_logger = logging.getLogger('hydraulics.compartments') derivatives_logger = logging.getLogger('hydraulics.derivatives') @@ -365,6 +374,9 @@ def initialize(self, population, soils): self.soils.clear() del self.initial_conditions[:] self.initial_conditions_mapping.clear() + if self.isolated_roots: + del self.initial_conditions_roots[:] + self.initial_conditions_mapping_roots.clear() # create new population and soil self.population.plants.extend(population.plants) @@ -480,10 +492,27 @@ def _init_initial_conditions(model_object, i): i += 1 return i + # initialize initial conditions roots + def _init_initial_conditions_roots(root_object, i): + compartments_names = Simulation.MODEL_COMPARTMENTS_NAMES[model.Organ] + self.initial_conditions_mapping_roots[root_object] = {} + for compartment_name in compartments_names: + if hasattr(root_object, compartment_name): + self.initial_conditions_mapping_roots[root_object][compartment_name] = i + self.initial_conditions_roots.append(0) + i += 1 + return i + i = 0 + i_root = 0 - for soil in soils.values(): - i = _init_initial_conditions(soil, i) + # We initialize soil only if roots are present + if self.cnwgrass_roots: + for soil in soils.values(): + if not self.isolated_roots: + i = _init_initial_conditions(soil, i) + else: + i_root = _init_initial_conditions_roots(soil, i_root) for plant in self.population.plants: i = _init_initial_conditions(plant, i) @@ -492,7 +521,17 @@ def _init_initial_conditions(model_object, i): for organ in (axis.roots, axis.xylem): if organ is None: continue - i = _init_initial_conditions(organ, i) + 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: + i_root = _init_initial_conditions_roots(organ, i_root) + self.first_initialization = False + + else: + i = _init_initial_conditions(organ, i) for phytomer in axis.phytomers: i = _init_initial_conditions(phytomer, i) if phytomer.hiddenzone: @@ -520,13 +559,24 @@ def run(self): self._interpolate_forcing() self._update_initial_conditions() + if self.isolated_roots and self.cnwgrass_roots: + self._update_initial_conditions_roots() if logger.isEnabledFor(logging.DEBUG): logger.debug("Run the solver with delta_t = %s", self.time_step) #: Call :func:`scipy.integrate.solve_ivp` to integrate the system over self.time_grid. - sol = solve_ivp(fun=self._calculate_all_derivatives, t_span=self.time_grid, y0=self.initial_conditions, - method='LSODA', t_eval=np.array([self.time_step]), dense_output=False) + if not self.isolated_roots: + sol = solve_ivp(fun=self._calculate_all_derivatives, t_span=self.time_grid, y0=self.initial_conditions, + method='LSODA', t_eval=np.array([self.time_step]), dense_output=False) + elif self.cnwgrass_roots: + 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) + 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: + 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) self.nfev_total += sol.nfev @@ -554,6 +604,14 @@ def _update_initial_conditions(self): for compartment_name, compartment_index in compartments.items(): self.initial_conditions[compartment_index] = getattr(model_object, compartment_name) + def _update_initial_conditions_roots(self): + """Update the compartments values in :attr:`initial_conditions` from the compartments values of :attr:`population` and :attr:`soils`. + """ + # Update the compartments values + for model_object, compartments in self.initial_conditions_mapping_roots.items(): + for compartment_name, compartment_index in compartments.items(): + self.initial_conditions_roots[compartment_index] = getattr(model_object, compartment_name) + def _interpolate_forcing(self): """Create functions to interpolate the forcing of the model to any time inside the time grid (see `self.time_grid`). diff --git a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py index ca25e51..eaa6d97 100644 --- a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py +++ b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py @@ -10,6 +10,7 @@ import numpy as np import math +from scipy.sparse import lil_matrix """ integration.cnmetabolism_facade @@ -452,9 +453,10 @@ def _initialize_indices_and_sparcity(self): # Collect per-axis element/HZ columns we must connect E_suc_cols, E_aa_cols = [], [] HZ_suc_cols, HZ_aa_cols = [], [] - - for variable in ('moistening', 'starch', 'proteins'): - setattr(axis.endosperm, f"_i_{variable}", icm[axis.phloem][variable]) + + 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 diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index bfd5e06..6d1374e 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -632,9 +632,9 @@ def __call__(self): self.hydraulics_facade_.run() # Update geometry - self.adel_wheat.update_geometry(g) - if show_3Dplant: - self.adel_wheat.plot(g) + self.adel_wheat.update_geometry(self.g) + if self.show_3Dplant: + self.adel_wheat.plot(self.g) # run Growth self.growth_facade_.run() diff --git a/src/openalea/cnwgrass/integration/hydraulics_facade.py b/src/openalea/cnwgrass/integration/hydraulics_facade.py index 8a9d718..65a7f24 100644 --- a/src/openalea/cnwgrass/integration/hydraulics_facade.py +++ b/src/openalea/cnwgrass/integration/hydraulics_facade.py @@ -74,7 +74,9 @@ def __init__(self, shared_mtg, delta_t, update_parameters, shared_elements_inputs_outputs_df, shared_organs_inputs_outputs_df, shared_soils_inputs_outputs_df, - update_shared_df=True): + update_shared_df=True, + isolated_roots=False, + cnwgrass_roots=True): """ :param openalea.mtg.mtg.MTG shared_mtg: The MTG shared between all models. :param int delta_t: The delta between two runs, in seconds. @@ -92,7 +94,7 @@ def __init__(self, shared_mtg, delta_t, update_parameters, self._shared_mtg = shared_mtg #: the MTG shared between all models - self._simulation = hydraulics_simulation.Simulation(delta_t=delta_t) + self._simulation = hydraulics_simulation.Simulation(delta_t=delta_t, isolated_roots=isolated_roots, cnwgrass_roots=cnwgrass_roots) 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) From f2b4a355be8f1a83849c51daf5508dceb9846dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Mon, 10 Aug 2026 07:38:02 +0200 Subject: [PATCH 10/21] debugging water shoot-root coupling --- src/openalea/cnwgrass/hydraulics/model.py | 38 +- .../cnwgrass/hydraulics/simulation.py | 380 +++++++++++++++++- .../cnwgrass/integration/component.py | 35 +- 3 files changed, 416 insertions(+), 37 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/model.py b/src/openalea/cnwgrass/hydraulics/model.py index 1e4674d..a0296b4 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 @@ -260,7 +260,7 @@ def calculate_xylem_water_potential(soil_water_potential, total_water_influx, Gr :param float soil_water_potential: MPa :param float total_water_influx: g H2O - :param float Growth: g H2O + :param float Growth: g H2O :param float delta_t: time step of the simulation (s) :return: Total water potential (MPa) @@ -292,7 +292,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 @@ -302,12 +302,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 @@ -377,11 +377,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 @@ -514,7 +514,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) @@ -720,14 +720,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 @@ -735,8 +735,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 @@ -845,11 +845,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/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index 455bf0e..919aec2 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -154,7 +154,7 @@ class Simulation(object): #: the fluxes exchanged between the compartments at organ scale ORGANS_FLUXES = [] #: the variables computed by integrating values of xylem components parameters/variables recursively - ORGANS_INTEGRATIVE_VARIABLES = [] + ORGANS_INTEGRATIVE_VARIABLES = ['water_outflux'] #: all the variables computed during a run step of the simulation at organ scale ORGANS_RUN_VARIABLES = ORGANS_STATE + ORGANS_INTERMEDIATE_VARIABLES + ORGANS_FLUXES + ORGANS_INTEGRATIVE_VARIABLES @@ -1029,6 +1029,384 @@ def _calculate_all_derivatives(self, t, y): y_derivatives[self.initial_conditions_mapping[soil]['water_content']] = soil.calculate_water_content_derivative(soil_water_outputs, soil.constant_water_content) + derivatives_logger = logging.getLogger('hydraulics.derivatives') + if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): + self._log_compartments(t_abs, y_derivatives, Simulation.LOGGERS_NAMES['derivatives']) + + return y_derivatives + + + def _calculate_shoot_derivatives(self, t, y): + """Compute the derivative of `y` at `t`. + :meth:`_calculate_all_derivatives` is passed as **func** argument to + :func:`solve_ivp(fun, t_span, y0,...) `. + :meth:`_calculate_all_derivatives` is called automatically by + :func:`scipy.integrate.solve_ivp `. + + First call to :meth:`_calculate_all_derivatives` uses `y` = **y0** and + `t` = **t_span** [0], where **y0** and **t_span** are arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + + Following calls to :meth:`_calculate_all_derivatives` use `t` in [**t_span** [0], **t_span** [1]]. + + :param float t: The current t at which we want to compute the derivatives. + Values of `t` are chosen automatically by :func:`scipy.integrate.solve_ivp`. + At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp` + `t` = **t_span** [0], where **t_span** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + For each following call to :meth:`_calculate_all_derivatives`, `t` belongs + to the interval [**t_span** [0], **t_span** [1]]. + :param list y: The current values of y. + At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp`, `y` = **y0** + where **y0** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + Then, values of `y` are chosen automatically by :func:`scipy.integrate.solve_ivp`. + + :return: The derivatives of `y` at `t`. + :rtype: list + """ + logger = logging.getLogger(__name__) + + if logger.isEnabledFor(logging.DEBUG): + t_abs = t + self.t_offset + logger.debug('t = {}'.format(t_abs)) + + if self.interpolate_forcing: + # Update state parameters using interpolation functions + for plant in self.population.plants: + for axis in plant.axes: + for phytomer in axis.phytomers: + for organ in (phytomer.lamina, phytomer.sheath): + if organ is None: + continue + for element in (organ.exposed_element, organ.enclosed_element): + if element is not None: + element_id = (plant.index, axis.label, phytomer.index, organ.label, element.label) + for forcing_label in Simulation.ELEMENTS_FORCING: + setattr(element, forcing_label, float(self.interpolation_functions[element_id][forcing_label](t))) + + # Compute integrative variables + self.population.calculate_aggregated_variables() + + compartments_logger = logging.getLogger('hydraulics.compartments') + if logger.isEnabledFor(logging.DEBUG) and compartments_logger.isEnabledFor(logging.DEBUG): + self._log_compartments(t_abs, y, Simulation.LOGGERS_NAMES['compartments']) + + # Check that the solver is not crashed + y_isnan = np.isnan(y) + if y_isnan.any(): + message = 'The solver did not manage to compute a compartment. See the logs. NaN found in y' + logger.exception(message) + raise SimulationRunError(message) + + y_derivatives = np.zeros_like(y) + + + #: Water flux with xylem and organs + for plant in self.population.plants: + for axis in plant.axes: + print("solver received", axis.xylem.water_potential) + + for phytomer in axis.phytomers: + # Hidden zone + hiddenzone = phytomer.hiddenzone + if hiddenzone is not None: + hiddenzone.leaf_L = y[self.initial_conditions_mapping[hiddenzone]['leaf_L']] + hiddenzone.thickness = y[self.initial_conditions_mapping[hiddenzone]['thickness']] + hiddenzone.width = y[self.initial_conditions_mapping[hiddenzone]['width']] + # Update of leaf_Lmax + hiddenzone.leaf_Lmax = hiddenzone.leaf_L + + if hiddenzone.leaf_pseudo_age == 0: #: First time after previous leaf emergence + #: Width and thickness + thickness_ratio = parameters.HIDDEN_ZONE_PARAMETERS.TL_ratio + width_ratio = parameters.HIDDEN_ZONE_PARAMETERS.WL_ratio + + hiddenzone.width = hiddenzone.leaf_L * width_ratio + hiddenzone.thickness = hiddenzone.leaf_L * thickness_ratio + hiddenzone.leaf_Wmax = hiddenzone.width + + #: Volume & water content as function of dimensions + hiddenzone.volume = hiddenzone.length * hiddenzone.width * hiddenzone.thickness + hiddenzone.water_content = hiddenzone.volume * parameters.RHO_WATER + #: Osmotic water potential + hiddenzone.osmotic_water_potential = hiddenzone.calculate_osmotic_water_potential(hiddenzone.fructan, hiddenzone.sucrose, hiddenzone.amino_acids, hiddenzone.volume, axis.SAM_temperature) + #: Total water potential + hiddenzone.water_potential = axis.xylem.water_potential + #: Turgor water potential + hiddenzone.turgor_water_potential = hiddenzone.water_potential - hiddenzone.osmotic_water_potential + #: Length + hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) + elif hiddenzone.leaf_pseudo_age > 0: #: After previous leaf emergence + #: Turgor water potential + hiddenzone.turgor_water_potential = y[self.initial_conditions_mapping[hiddenzone]['turgor_water_potential']] + #: Volume and water content + hiddenzone.water_content = y[self.initial_conditions_mapping[hiddenzone]['water_content']] + hiddenzone.volume = hiddenzone.calculate_volume(hiddenzone.water_content) + #: Osmotic water potential + hiddenzone.osmotic_water_potential = hiddenzone.calculate_osmotic_water_potential(hiddenzone.fructan, hiddenzone.sucrose, hiddenzone.amino_acids, hiddenzone.volume, axis.SAM_temperature) + #: Total water potential + hiddenzone.water_potential = hiddenzone.calculate_water_potential(hiddenzone.turgor_water_potential, hiddenzone.osmotic_water_potential) + #: Length + hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) + else: #: Before previous leaf emergence (calculation in morphogenesis) + continue + + #: Resistance to water flow + hiddenzone_dimensions = {'length': hiddenzone.length, 'thickness': hiddenzone.thickness, 'width': hiddenzone.width} + hiddenzone.resistance = hiddenzone.calculate_resistance(hiddenzone_dimensions) + #: Flows with xylem + hiddenzone.water_influx = hiddenzone.calculate_water_flux(hiddenzone.water_potential, axis.xylem.water_potential, hiddenzone.resistance, self.delta_t) + hiddenzone.water_outflow = 0 #: No water flow between hiddenzone and element + + + # Photosynthetic Organ Elements + # for organ in (phytomer.lamina, phytomer.internode, phytomer.sheath): + for organ in (phytomer.lamina, phytomer.sheath): + if organ is None: + continue + for element in (organ.exposed_element, organ.enclosed_element): + if element is None: + continue + element.length = y[self.initial_conditions_mapping[element]['length']] + element.width = y[self.initial_conditions_mapping[element]['width']] + element.thickness = y[self.initial_conditions_mapping[element]['thickness']] + element.organ_dimensions = {'length': element.length, 'width': element.width, 'thickness': element.thickness} + + if element.age == 0: #: First time after element emergence + #: Width and thickness of the hiddenzone + if organ.label == "blade": + element.thickness = hiddenzone.thickness + element.width = hiddenzone.width + + #: Volume and water content as function of dimensions + element.volume = element.calculate_organ_volume(element.organ_dimensions) + element.water_content = element.volume * parameters.RHO_WATER + # : Osmotic water potential + element.osmotic_water_potential = element.calculate_osmotic_water_potential(element.sucrose, element.amino_acids, element.volume, element.temperature, element.fructan) + #: Total water potential + element.water_potential = axis.xylem.water_potential + # : Turgor water potential + element.turgor_water_potential = element.water_potential - element.osmotic_water_potential + + # Length of the HZ + if hiddenzone is not None: + if organ.label == "blade": + hiddenzone.length = hiddenzone.length_hz_En + + elif element.age > 0: #: Emerged element + #: Water content + element.water_content = y[self.initial_conditions_mapping[element]['water_content']] + # : Volume + element.volume = element.calculate_volume(element.water_content) + # : Turgor water potential + element.turgor_water_potential = y[self.initial_conditions_mapping[element]['turgor_water_potential']] + #: Osmotic water potential + element.osmotic_water_potential = element.calculate_osmotic_water_potential(element.sucrose, element.amino_acids, element.volume, element.temperature, element.fructan) + #: Total water potential + element.water_potential = element.calculate_water_potential(element.turgor_water_potential, element.osmotic_water_potential) + + # # Length of the HZ + if hiddenzone is not None: + if organ.label == "blade": + hiddenzone.length = hiddenzone.length_hz_En + + #: Resistance to water flow + element.resistance = element.calculate_resistance(element.organ_dimensions) + #: Water fluxes with xylem + element.water_influx = element.calculate_water_flux(element.water_potential, axis.xylem.water_potential, element.resistance, self.delta_t) + + + #: compute the derivative of each compartment of element + for plant in self.population.plants: + for axis in plant.axes: + for phytomer in axis.phytomers: + # Hidden zone + hiddenzone = phytomer.hiddenzone + if hiddenzone is not None: + #: Delta water content + delta_water_content_hz = hiddenzone.calculate_delta_water_content(hiddenzone.water_influx, hiddenzone.water_outflow) + #: Extensibility + hiddenzone_delta_teq = hiddenzone.calculate_time_equivalent_Tref(axis.SAM_temperature, self.delta_t) + phi = hiddenzone.calculate_extensibility_temperature(hiddenzone.leaf_pseudo_age, hiddenzone_delta_teq, self.delta_t) + hiddenzone.phi_length = phi['z'] # extensibility for length + hiddenzone.phi_width = phi['x'] # extensibility for length + hiddenzone.phi_thickness = phi['y'] # extensibility for length + hiddenzone.phi_volume = phi['x'] + phi['y'] + phi['z'] # volumetric extensibility + #: Elasticity + epsilon_x, epsilon_y, epsilon_z = hiddenzone.PARAMETERS.epsilon['x'], hiddenzone.PARAMETERS.epsilon['y'], hiddenzone.PARAMETERS.epsilon['z'] + hiddenzone.epsilon_volume = (epsilon_x * epsilon_y * epsilon_z) / (epsilon_z * epsilon_x + epsilon_z * epsilon_y + epsilon_x * epsilon_y) #: Elastic reversible growth (MPa) + + if hiddenzone.leaf_pseudo_age > 0: #: After previous leaf emergence + #: Derivatives + #: Delta turgor pressure + delta_turgor_water_potential = hiddenzone.calculate_delta_turgor_water_potential(phi, hiddenzone.turgor_water_potential, hiddenzone.volume, delta_water_content_hz) + #: Dimensions with plastic and elastic deformation + hiddenzone_dimensions = {'length': hiddenzone.length, 'width': hiddenzone.width, 'thickness': hiddenzone.thickness} + delta_hiddenzone_dimensions_plastic = hiddenzone.calculate_delta_organ_dimensions_plastic(hiddenzone.turgor_water_potential, phi, hiddenzone_dimensions) + delta_hiddenzone_dimensions_elastic = hiddenzone.calculate_delta_organ_dimensions_elastic(delta_turgor_water_potential, hiddenzone_dimensions) + + # Saving into outputs + hiddenzone.delta_hiddenzone_dimensions_plastic = delta_hiddenzone_dimensions_plastic['length'] * 1000 + + if hiddenzone.leaf_L >= hiddenzone.leaf_pseudostem_length: # Emerged blade + # Growing leaf with emerged blade + y_derivatives[self.initial_conditions_mapping[hiddenzone]['water_content']] = 0 # Transfer of water content to emerged element + y_derivatives[self.initial_conditions_mapping[hiddenzone]['turgor_water_potential']] = delta_turgor_water_potential + # Elastic deformation + y_derivatives[self.initial_conditions_mapping[hiddenzone]['width']] = delta_hiddenzone_dimensions_elastic['width'] + y_derivatives[self.initial_conditions_mapping[hiddenzone]['thickness']] = delta_hiddenzone_dimensions_elastic['thickness'] + y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] = delta_hiddenzone_dimensions_elastic['length'] + + else: # Enclosed blade + y_derivatives[self.initial_conditions_mapping[hiddenzone]['water_content']] = delta_water_content_hz + y_derivatives[self.initial_conditions_mapping[hiddenzone]['turgor_water_potential']] = delta_turgor_water_potential + # Plastic deformation + y_derivatives[self.initial_conditions_mapping[hiddenzone]['thickness']] = delta_hiddenzone_dimensions_elastic['thickness'] + delta_hiddenzone_dimensions_plastic['thickness'] + y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] = delta_hiddenzone_dimensions_elastic['length'] + delta_hiddenzone_dimensions_plastic['length'] + y_derivatives[self.initial_conditions_mapping[hiddenzone]['width']] = delta_hiddenzone_dimensions_elastic['width'] + delta_hiddenzone_dimensions_plastic['width'] + hiddenzone.delta_leaf_L = y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] + + hiddenzone.leaf_Wmax += delta_hiddenzone_dimensions_plastic['width'] + hiddenzone.organ_volume = hiddenzone.calculate_organ_volume(hiddenzone_dimensions) + hiddenzone.WC_mstruct = hiddenzone.water_content / (hiddenzone.water_content + hiddenzone.mstruct) * 100 + + else: #: Before previous leaf emergence + continue + + + # Photosynthetic Organ Elements + # for organ in (phytomer.lamina, phytomer.internode, phytomer.sheath): + for organ in (phytomer.lamina, phytomer.sheath): + if organ is None: + continue + for element in (organ.exposed_element, organ.enclosed_element): + if element is None: + continue + epsilon_x, epsilon_y, epsilon_z = element.PARAMETERS.epsilon['x'], element.PARAMETERS.epsilon['y'], element.PARAMETERS.epsilon['z'] + element.epsilon_volume = (epsilon_z * epsilon_x * epsilon_y) / (epsilon_z * epsilon_x + epsilon_z * epsilon_y + epsilon_x * epsilon_y) #: Elastic reversible growth (MPa) + element.organ_dimensions = {'length': element.length, 'width': element.width, 'thickness': element.thickness} + #: Delta water content + delta_water_content_ele = element.calculate_delta_water_content(element.water_influx, element.Total_Transpiration_turgor) + #: Delta turgor pressure + delta_turgor_water_potential = element.calculate_delta_turgor_water_potential(element.volume, delta_water_content_ele) + #: Dimensions + delta_element_dimensions = element.calculate_delta_organ_dimensions(delta_turgor_water_potential, element.organ_dimensions) + #: Derivatives + y_derivatives[self.initial_conditions_mapping[element]['turgor_water_potential']] = delta_turgor_water_potential + + if hiddenzone is not None: # Growing leaf + if element.is_growing == True: # Growing element + y_derivatives[self.initial_conditions_mapping[element]['water_content']] = delta_water_content_ele + delta_water_content_hz # Transfer of water content from hiddenzone + # Plastic deformation + y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] = delta_hiddenzone_dimensions_plastic['length'] + delta_element_dimensions['length'] + y_derivatives[self.initial_conditions_mapping[element]['length']] = delta_hiddenzone_dimensions_plastic['length'] + delta_element_dimensions['length'] + y_derivatives[self.initial_conditions_mapping[element]['width']] = delta_hiddenzone_dimensions_plastic['width'] + delta_element_dimensions['width'] + y_derivatives[self.initial_conditions_mapping[element]['thickness']] = delta_hiddenzone_dimensions_plastic['thickness'] + delta_element_dimensions['thickness'] + y_derivatives[self.initial_conditions_mapping[element]['length']] = delta_hiddenzone_dimensions_plastic['length'] + delta_element_dimensions['length'] + + elif element.is_growing == False: # End of leaf elongation + #: Derivatives + y_derivatives[self.initial_conditions_mapping[element]['water_content']] = delta_water_content_ele + # Elastic deformation + y_derivatives[self.initial_conditions_mapping[element]['length']] = delta_element_dimensions['length'] + y_derivatives[self.initial_conditions_mapping[element]['width']] = delta_element_dimensions['width'] + y_derivatives[self.initial_conditions_mapping[element]['thickness']] = delta_element_dimensions['thickness'] + y_derivatives[self.initial_conditions_mapping[hiddenzone]['leaf_L']] = delta_element_dimensions['length'] + + # if hiddenzone.leaf_is_growing == False: + if hiddenzone.leaf_pseudo_age >= hiddenzone.PARAMETERS.tend: + # Length of HT at the end of elongation - UPDATE VICTORIA 07.01.25 + hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) + + else: # Mature leaf + #: Derivatives + y_derivatives[self.initial_conditions_mapping[element]['water_content']] = delta_water_content_ele + # Elastic deformation + y_derivatives[self.initial_conditions_mapping[element]['length']] = delta_element_dimensions['length'] + y_derivatives[self.initial_conditions_mapping[element]['width']] = delta_element_dimensions['width'] + y_derivatives[self.initial_conditions_mapping[element]['thickness']] = delta_element_dimensions['thickness'] + + #: Dimensions volume of element + element.organ_volume = element.calculate_organ_volume(element.organ_dimensions) + element.WC_mstruct = element.water_content / (element.water_content + element.mstruct) * 100 + + axis.xylem.water_outflux = axis.water_influx + axis.Growth # Mimic what is imposed to soil when roots are present + + derivatives_logger = logging.getLogger('hydraulics.derivatives') + if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): + self._log_compartments(t_abs, y_derivatives, Simulation.LOGGERS_NAMES['derivatives']) + + return y_derivatives + + + def _calculate_root_derivatives(self, t, y): + """Compute the derivative of `y` at `t`. + :meth:`_calculate_all_derivatives` is passed as **func** argument to + :func:`solve_ivp(fun, t_span, y0,...) `. + :meth:`_calculate_all_derivatives` is called automatically by + :func:`scipy.integrate.solve_ivp `. + + First call to :meth:`_calculate_all_derivatives` uses `y` = **y0** and + `t` = **t_span** [0], where **y0** and **t_span** are arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + + Following calls to :meth:`_calculate_all_derivatives` use `t` in [**t_span** [0], **t_span** [1]]. + + :param float t: The current t at which we want to compute the derivatives. + Values of `t` are chosen automatically by :func:`scipy.integrate.solve_ivp`. + At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp` + `t` = **t_span** [0], where **t_span** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + For each following call to :meth:`_calculate_all_derivatives`, `t` belongs + to the interval [**t_span** [0], **t_span** [1]]. + :param list y: The current values of y. + At first call to :meth:`_calculate_all_derivatives` by :func:`scipy.integrate.solve_ivp`, `y` = **y0** + where **y0** is one of the arguments passed to :func:`solve_ivp(fun, t_span, y0,...) `. + Then, values of `y` are chosen automatically by :func:`scipy.integrate.solve_ivp`. + + :return: The derivatives of `y` at `t`. + :rtype: list + """ + logger = logging.getLogger(__name__) + + if logger.isEnabledFor(logging.DEBUG): + t_abs = t + self.t_offset + logger.debug('t = {}'.format(t_abs)) + + # Compute integrative variables + self.population.calculate_aggregated_variables() + + compartments_logger = logging.getLogger('hydraulics.compartments') + if logger.isEnabledFor(logging.DEBUG) and compartments_logger.isEnabledFor(logging.DEBUG): + self._log_compartments(t_abs, y, Simulation.LOGGERS_NAMES['compartments']) + + # Check that the solver is not crashed + y_isnan = np.isnan(y) + if y_isnan.any(): + message = 'The solver did not manage to compute a compartment. See the logs. NaN found in y' + logger.exception(message) + raise SimulationRunError(message) + + y_derivatives = np.zeros_like(y) + + # TODO: TEMP !!!! + soil_water_outputs = 0 + soil = self.soils[(1, 'MS')] + soil.water_content = y[self.initial_conditions_mapping_roots[soil]['water_content']] + soil.SRWC = soil.calculate_SRWC(soil.water_content) + soil.water_potential = soil.calculate_water_potential(soil.SRWC) + + + #: Water flux with xylem and organs + for plant in self.population.plants: + for axis in plant.axes: + # Xylem + #: Total water potential + axis.xylem.water_potential = axis.xylem.calculate_xylem_water_potential(soil.water_potential, axis.water_influx, axis.Growth, self.delta_t) + + # Store water used from soil for eahc axis + soil_water_outputs += (axis.water_influx + axis.Growth) + + # compute the derivative of each compartment of soil + y_derivatives[self.initial_conditions_mapping_roots[soil]['water_content']] = soil.calculate_water_content_derivative(soil_water_outputs, soil.constant_water_content) + + derivatives_logger = logging.getLogger('hydraulics.derivatives') if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): self._log_compartments(t_abs, y_derivatives, Simulation.LOGGERS_NAMES['derivatives']) diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index 6d1374e..ff77a8c 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -48,22 +48,19 @@ class CNW_Grass(Model): variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") Unloading_Amino_Acids_phloem: float = declare(default=0.1, unit="umol.h-1", unit_comment="of N", min_value="", max_value="", description="", value_comment="", references="", DOI="", - variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") - sucrose_phloem_outside_solve: float = declare(default=10., unit="µmol of C", unit_comment="amount in equivalent C", - min_value="", max_value="", description="", value_comment="", references="", DOI="", - variable_type="input", by="root_carbon", state_variable_type="extensive", edit_by="user") - amino_acids_phloem_outside_solve: float = declare(default=1., unit="µmol of N", unit_comment="amount in equivalent N", - min_value="", max_value="", description="", value_comment="", references="", DOI="", - variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") + variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") cytokinins: float = declare(default=0., unit="AU", unit_comment="", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="input", by="root_nitrogen", state_variable_type="extensive", edit_by="user") mstruct: float = declare(default=0., unit="g", unit_comment="", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="input", by="root_carbon", state_variable_type="extensive", edit_by="user") + xylem_water_potential_collar: float = declare(default=-0.05, unit="MPa", unit_comment="", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_water", state_variable_type="extensive", edit_by="user") # State variables condidered as outputs to bellowground models - Total_Transpiration: float = declare(default=0., unit="mmol.s-1", unit_comment="of water", + water_outflux: float = declare(default=0., unit="g.time_step-1", unit_comment="of water", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") mstruct_axis: float = declare(default=0.05, unit="g", unit_comment="of axis structural mass", @@ -440,15 +437,11 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.cnw_grass_root_props = self.g.get_vertex_property(2)["roots"] - # TODO GB : Temporary + # TODO GB : Temporary inputs initialization at Component's prescribed value self.cnw_grass_root_props["Unloading_Sucrose"] = self.props["Unloading_Sucrose"][1] self.cnw_grass_root_props["Unloading_Amino_Acids"] = self.props["Unloading_Amino_Acids"][1] - self.g.properties()["Total_Transpiration"][2] = self.props["Total_Transpiration"][1] + self.g.get_vertex_property(2)['xylem']['water_potential'] = self.props["xylem_water_potential_collar"][1] - self.g.get_vertex_property(2)['phloem']['sucrose'] = self.props["sucrose_phloem"][1] - self.g.get_vertex_property(2)['phloem']['amino_acids'] = self.props["amino_acids_phloem"][1] - - if self.synchronize_adventitious_emergence: # Specific initialization for already emerged leaves at the begining of the simulation self.main_stem_vid = [v for v in self.g.vertices(scale=2) if "MS" in str(self.g.node(v).label)][0] @@ -477,9 +470,12 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.props["adventitious_to_emerge"].update({1: nodal_emergence_delays}) - # TODO GB Temporary while an initialization bug for shared mtg is still present + # TODO GB Temporary initialization of model's outputs with component's prescribed values + self.g.get_vertex_property(2)['phloem']['sucrose'] = self.props["sucrose_phloem"][1] + self.g.get_vertex_property(2)['phloem']['amino_acids'] = self.props["amino_acids_phloem"][1] self.g.get_vertex_property(2)['phloem']['Unloading_Sucrose_shoot_organs'] = 30. self.g.get_vertex_property(2)['phloem']['Unloading_Amino_Acids_shoot_organs'] = 1. + self.g.get_vertex_property(2)['xylem']['water_outflux'] = 0. self.sync_shoot_outputs_with_root_mtg() @@ -491,6 +487,11 @@ def sync_shoot_inputs_with_shoot_mtg(self): elif name == "Unloading_Amino_Acids_phloem": self.cnw_grass_root_props["Unloading_Amino_Acids"] = self.props[name][1] / self.props['mstruct'][1] + + elif name == 'xylem_water_potential_collar': + self.g.get_vertex_property(2)['xylem']['water_potential'] = self.props[name][1] + print("received value", self.g.get_vertex_property(2)['xylem']['water_potential']) + else: self.cnw_grass_root_props[name] = self.props[name][1] @@ -498,8 +499,8 @@ def sync_shoot_outputs_with_root_mtg(self): # Link this specific data structure to self for variables exchange, only for outputs that will be read by other models here. # Note : here eval is necessary to ensure intended lambda function definition for name in self.state_variables: - if name == "Total_Transpiration": - self.props[name].update({1: self.g.get_vertex_property(2)[name]}) + if name == "water_outflux": + self.props[name].update({1: self.g.get_vertex_property(2)['xylem']['water_outflux']}) elif name == "mstruct_axis": self.props[name].update({1: self.g.get_vertex_property(2)['mstruct']}) From b55e301d6ede646694d96aec739e413df9bf5e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Mon, 10 Aug 2026 08:20:08 +0200 Subject: [PATCH 11/21] Manual exclusion from initialized outputs was necessary for xylem.water_potential to be exported --- src/openalea/cnwgrass/hydraulics/simulation.py | 2 -- src/openalea/cnwgrass/integration/component.py | 1 - src/openalea/cnwgrass/integration/hydraulics_facade.py | 7 ++++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index 919aec2..906708f 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -1102,8 +1102,6 @@ def _calculate_shoot_derivatives(self, t, y): #: Water flux with xylem and organs for plant in self.population.plants: for axis in plant.axes: - print("solver received", axis.xylem.water_potential) - for phytomer in axis.phytomers: # Hidden zone hiddenzone = phytomer.hiddenzone diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index ff77a8c..b3e1082 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -490,7 +490,6 @@ def sync_shoot_inputs_with_shoot_mtg(self): elif name == 'xylem_water_potential_collar': self.g.get_vertex_property(2)['xylem']['water_potential'] = self.props[name][1] - print("received value", self.g.get_vertex_property(2)['xylem']['water_potential']) else: self.cnw_grass_root_props[name] = self.props[name][1] diff --git a/src/openalea/cnwgrass/integration/hydraulics_facade.py b/src/openalea/cnwgrass/integration/hydraulics_facade.py index 65a7f24..b006932 100644 --- a/src/openalea/cnwgrass/integration/hydraulics_facade.py +++ b/src/openalea/cnwgrass/integration/hydraulics_facade.py @@ -117,6 +117,8 @@ def __init__(self, shared_mtg, delta_t, update_parameters, hydraulics_organs_data_df=model_organs_inputs_df, hydraulics_soils_data_df=model_soils_inputs_df) + self.isolated_roots = isolated_roots + def run(self, update_shared_df=False): """ @@ -229,7 +231,10 @@ def _initialize_model(self): 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] - hydraulics_organ_data_names = set(hydraulics_simulation.Simulation.ORGANS_STATE).intersection(hydraulics_organ.__dict__) + access_mtg_names = hydraulics_simulation.Simulation.ORGANS_STATE + if hydraulics_organ_class == hydraulics_model.Xylem and self.isolated_roots: + access_mtg_names += ["water_potential"] + 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: From 9b7a9447ba1da9af654ab9e31a6e236e76d796a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Mon, 10 Aug 2026 08:46:39 +0200 Subject: [PATCH 12/21] docstring --- .../cnwgrass/integration/component.py | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index b3e1082..e741190 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -111,7 +111,64 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, 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): - + """ + :param openalea.mtg.mtg.MTG root_mtg: the MTG of the belowground root model this shoot component is coupled to (e.g. Root-BRIDGES). Its vertex-1 properties are the channel through which the two models + exchange state at each time step: this component reads them as its declared ``input`` variables (``Export_Nitrates``, ``Export_Amino_Acids``, ``Unloading_Sucrose_phloem``, + ``Unloading_Amino_Acids_phloem``, ``cytokinins``, ``mstruct``, ``xylem_water_potential_collar``) and writes back its declared ``state_variable`` outputs + (``water_outflux``, ``mstruct_axis``, ``sucrose_phloem``, ``amino_acids_phloem``, ``Unloading_Sucrose``, ``Unloading_Amino_Acids``, + ``Unloading_Sucrose_shoot_organs``, ``Unloading_Amino_Acids_shoot_organs``, ``Export_cytokinins``, ``adventitious_to_emerge``). + :param pandas.DataFrame meteo: hourly meteorological forcing, indexed by time step ``t`` (hours), with at least the columns ``PARi``, ``DOY``, ``hour``, ``air_temperature``, ``ambient_CO2``, ``humidity``, + ``Wind`` and ``soil_temperature``. + :param dict [str, pandas.DataFrame] inputs_dataframes: the initial-state dataframes of the shoot model, keyed by the corresponding ``*_INITIAL_STATE_FILENAME`` argument + (one entry each for hiddenzones, elements, axes, organs and soils). Typically built by :func:`scenario_utility`. + :param str HIDDENZONES_INITIAL_STATE_FILENAME: key of `inputs_dataframes` holding the initial state of the hiddenzones (growing leaf/internode bases). + :param str ELEMENTS_INITIAL_STATE_FILENAME: key of `inputs_dataframes` holding the initial state of the photosynthetic and non-photosynthetic organ elements. + :param str AXES_INITIAL_STATE_FILENAME: key of `inputs_dataframes` holding the initial state at axis scale. + :param dict or None update_parameters_all_models: a dict to override default model parameters, keyed by sub-model name + {'cnmetabolism': {'organ1': {'param1': 'val1', 'param2': 'val2'}, 'organ2': {...}}, 'morphogenesis': {'param1': 'val1', ...}, ...}. + :param dict or None step_callback: a dict of functions used to force some external inputs that are natively computed by the model, keyed by function name. Recognised keys are + ``'ADEL_mtg'`` (called as ``step_callback['ADEL_mtg'](adel_wheat, INPUTS_DIRPATH, nff)`` to build the initial MTG instead of loading a serialised one) + and ``'nitrate_uptake'`` (called at each time step as ``step_callback['nitrate_uptake'](t, population, g)`` when `external_soil_model` is True). + :param int HOUR_TO_SECOND_CONVERSION_FACTOR: number of seconds in one hour, used to convert the `*_TIMESTEP` arguments (in hours) into a `delta_t` in seconds for each facade. + :param str ORGANS_INITIAL_STATE_FILENAME: key of `inputs_dataframes` holding the initial state of the non-photosynthetic organs (roots, grains, ...). + :param str SOILS_INITIAL_STATE_FILENAME: key of `inputs_dataframes` holding the initial state at soil scale. + :param str INPUTS_DIRPATH: path of the directory containing the model inputs (serialised ADEL-Wheat scene/MTG, ``phytoT.csv``, ...). + :param bool single_plant: if True, build a single isolated plant with :class:`AdelDyn` instead of a stand generated from `plant_density` and `inter_row`. + :param dict [int, float] plant_density: the density of plants (plants.m-2), one key per plant id. + :param float inter_row: the inter-row distance (m), used to build the canopy stand when `single_plant` is False. + :param float sowing_depth: the sowing depth (m, stored as `self.Zsowing`), used in the perceived-temperature computation of the morphogenesis model. + :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. + :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 + through the `step_callback['nitrate_uptake']` function. + :param bool heterogeneous_canopy: whether to create a duplicated heterogeneous canopy from the initial MTG (passed to the Caribu facade at each run). + :param bool show_3Dplant: whether to plot the 3D scene with PlantGL after each geometry update. + :param bool hydraulics: if True, couple the model to the turgor-driven hydraulics model (xylem water potentials and turgor-driven growth); also enables the 'Tuzet' and 'hydraulics' stomatal + conductance models. + :param str stomatal_model_name: the model of stomatal conductance. Should be one of 'BWB', 'Leuning', 'Tuzet' or 'hydraulics'. 'Tuzet' and 'hydraulics' require `hydraulics` to be True. + :param dict or None drought_trigger: a dict to define an external drought-control scenario, {'trigger_variable': value}. For now only implemented for the 'green_area' variable + (value = total green area below which the drought is triggered). Only used if `hydraulics` and `cnwgrass_roots` are True. + :param dict or None rehydration_scenario: a dict to specify the rehydration scenario following a drought event, + {'stop_drought_SRWC': SRWC at which the drought event stops (%), 'SRWC_target': target SRWC for rehydration (%), 'rehydration_duration': duration of the + rehydration period (days)}. Only used if `hydraulics` and `cnwgrass_roots` are True. + :param bool optimal_growth_option: if True, the morphogenesis model assumes optimal growth conditions instead of metabolism-limited growth. + :param bool option_static: whether the model should be run on a static plant architecture (no new organ initiation from morphogenesis). + :param bool isolated_roots: if True, the root/xylem compartments are integrated by the ODE solvers separately from the shoot compartments, so that roots can be driven by another, + independently-stepped model. Forced to True whenever `cnwgrass_roots` is False. + :param bool cnwgrass_roots: whether the CNW-Grass root model is used to represent the roots and soil. Set to False when roots and soil are entirely delegated to a coupled belowground + model (e.g. Root-BRIDGES via `root_mtg`), in which case `isolated_roots` is forced to True. + :param bool UPDATE_SHARED_DF: if True, update the shared inputs/outputs dataframes of every facade at init and after each run; otherwise they are only filled when outputs are stored. + :param int START_TIME: the first time step of the simulation (hours), used to initialise `self.time_step_in_hours` and, when resuming from previous outputs, as the first index read from `meteo`. + :param int CARIBU_TIMESTEP: number of hours between two calls to the Caribu light-interception facade. + :param int MORPHOGENESIS_TIMESTEP: number of hours between two calls to the morphogenesis facade. + :param int GROWTH_TIMESTEP: number of hours between two calls to the growth facade. + :param int CNMETABOLISM_TIMESTEP: number of hours between two calls to the CN-metabolism facade. + :param int SENESCENCE_TIMESTEP: number of hours between two calls to the senescence facade; also used as the increment of `self.time_step_in_hours` at the end of each `__call__`. + :param int HYDRAULICS_TIMESTEP: number of hours between two calls to the hydraulics facade (used only if `hydraulics` is True). + """ # SELF STORAGE FOR LOOP PARAMETERS self.meteo = meteo From a56192fd7f498d779f2619cc3cfa5ed97dd3753c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Mon, 10 Aug 2026 16:18:17 +0200 Subject: [PATCH 13/21] remove debug prints --- src/openalea/cnwgrass/morphogenesis/simulation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/openalea/cnwgrass/morphogenesis/simulation.py b/src/openalea/cnwgrass/morphogenesis/simulation.py index 4420912..ead0529 100644 --- a/src/openalea/cnwgrass/morphogenesis/simulation.py +++ b/src/openalea/cnwgrass/morphogenesis/simulation.py @@ -165,7 +165,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 @@ -387,7 +387,6 @@ def run(self, Tair, Tsoil, Zsowing=0.025): # TODO: besoin correction pour savoir a quel pas de temps exact?? curr_hiddenzone_outputs['leaf_is_emerged'] = self.model.calculate_leaf_emergence(hiddenzone_inputs['leaf_L'], leaf_pseudostem_length) if curr_hiddenzone_outputs['leaf_is_emerged']: # Initialise lamina outputs - print('leaf emergence', hiddenzone_id) new_lamina = self.element_init.__dict__.copy() self.outputs['elements'][lamina_id] = new_lamina From c41e8cf28522aef0277969fcd0d8cbc8728da951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Wed, 26 Aug 2026 14:41:58 +0200 Subject: [PATCH 14/21] Shoot-root coupling homogeneous xylem compartment --- src/openalea/cnwgrass/hydraulics/model.py | 2 ++ .../cnwgrass/hydraulics/simulation.py | 22 +++++++++++++--- .../cnwgrass/integration/component.py | 25 ++++++++++++------- .../cnwgrass/integration/hydraulics_facade.py | 2 +- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/model.py b/src/openalea/cnwgrass/hydraulics/model.py index a0296b4..3bf4e0c 100644 --- a/src/openalea/cnwgrass/hydraulics/model.py +++ b/src/openalea/cnwgrass/hydraulics/model.py @@ -248,6 +248,8 @@ def __init__(self, label='xylem', water_potential=INIT_COMPARTMENTS.water_potent # state parameters self.water_potential = water_potential #: MPa + self.root_xylem_water_potential = water_potential #: MPa first initialized for 0 gradient + self.shoot_root_xylem_conductance = 1e-2 # g.MPa-1.s-1 # integrative variables self.delta_t = 3600 #: the delta t of the simulation (in seconds) diff --git a/src/openalea/cnwgrass/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index 906708f..68039af 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -150,11 +150,11 @@ class Simulation(object): #: of the compartments associated to each organ (see :attr:`MODEL_COMPARTMENTS_NAMES`) ORGANS_STATE = ORGANS_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.Organ, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at organ scale - ORGANS_INTERMEDIATE_VARIABLES = ['water_potential'] + ORGANS_INTERMEDIATE_VARIABLES = ['water_potential', 'root_xylem_water_potential', 'shoot_root_xylem_conductance'] #: the fluxes exchanged between the compartments at organ scale ORGANS_FLUXES = [] #: the variables computed by integrating values of xylem components parameters/variables recursively - ORGANS_INTEGRATIVE_VARIABLES = ['water_outflux'] + ORGANS_INTEGRATIVE_VARIABLES = [] #: all the variables computed during a run step of the simulation at organ scale ORGANS_RUN_VARIABLES = ORGANS_STATE + ORGANS_INTERMEDIATE_VARIABLES + ORGANS_FLUXES + ORGANS_INTEGRATIVE_VARIABLES @@ -577,6 +577,12 @@ def run(self): else: 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) + 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) self.nfev_total += sol.nfev @@ -1098,6 +1104,8 @@ def _calculate_shoot_derivatives(self, t, y): y_derivatives = np.zeros_like(y) + sum_organs_kr = 0. + sum_organs_kr_psi = 0. #: Water flux with xylem and organs for plant in self.population.plants: @@ -1154,6 +1162,9 @@ def _calculate_shoot_derivatives(self, t, y): hiddenzone.water_influx = hiddenzone.calculate_water_flux(hiddenzone.water_potential, axis.xylem.water_potential, hiddenzone.resistance, self.delta_t) hiddenzone.water_outflow = 0 #: No water flow between hiddenzone and element + # Positionned here to capture the last computation of the last loop + sum_organs_kr += hiddenzone.nb_replications / hiddenzone.resistance + sum_organs_kr_psi += hiddenzone.nb_replications * hiddenzone.water_potential / hiddenzone.resistance # Photosynthetic Organ Elements # for organ in (phytomer.lamina, phytomer.internode, phytomer.sheath): @@ -1211,6 +1222,8 @@ def _calculate_shoot_derivatives(self, t, y): #: Water fluxes with xylem element.water_influx = element.calculate_water_flux(element.water_potential, axis.xylem.water_potential, element.resistance, self.delta_t) + sum_organs_kr += element.nb_replications / element.resistance # TODO check replication for elements + sum_organs_kr_psi += element.nb_replications * element.water_potential / element.resistance #: compute the derivative of each compartment of element for plant in self.population.plants: @@ -1325,8 +1338,9 @@ def _calculate_shoot_derivatives(self, t, y): #: Dimensions volume of element element.organ_volume = element.calculate_organ_volume(element.organ_dimensions) element.WC_mstruct = element.water_content / (element.water_content + element.mstruct) * 100 - - axis.xylem.water_outflux = axis.water_influx + axis.Growth # Mimic what is imposed to soil when roots are present + + self.sum_organs_kr = sum_organs_kr + self.sum_organs_kr_psi = sum_organs_kr_psi derivatives_logger = logging.getLogger('hydraulics.derivatives') if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index e741190..dd2b05d 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -55,12 +55,15 @@ class CNW_Grass(Model): mstruct: float = declare(default=0., unit="g", unit_comment="", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="input", by="root_carbon", state_variable_type="extensive", edit_by="user") - xylem_water_potential_collar: float = declare(default=-0.05, unit="MPa", unit_comment="", + root_xylem_water_potential: float = declare(default=-0.1, unit="MPa", unit_comment="", + min_value="", max_value="", description="", value_comment="", references="", DOI="", + variable_type="input", by="root_water", state_variable_type="extensive", edit_by="user") + shoot_root_xylem_conductance: float = declare(default=1e-2, unit="g.MPa-1.s-1", unit_comment="of water", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="input", by="root_water", state_variable_type="extensive", edit_by="user") # State variables condidered as outputs to bellowground models - water_outflux: float = declare(default=0., unit="g.time_step-1", unit_comment="of water", + xylem_water_potential: float = declare(default=-0.1, unit="MPa", unit_comment="", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") mstruct_axis: float = declare(default=0.05, unit="g", unit_comment="of axis structural mass", @@ -114,7 +117,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, """ :param openalea.mtg.mtg.MTG root_mtg: the MTG of the belowground root model this shoot component is coupled to (e.g. Root-BRIDGES). Its vertex-1 properties are the channel through which the two models exchange state at each time step: this component reads them as its declared ``input`` variables (``Export_Nitrates``, ``Export_Amino_Acids``, ``Unloading_Sucrose_phloem``, - ``Unloading_Amino_Acids_phloem``, ``cytokinins``, ``mstruct``, ``xylem_water_potential_collar``) and writes back its declared ``state_variable`` outputs + ``Unloading_Amino_Acids_phloem``, ``cytokinins``, ``mstruct``, ``xylem_water_potential``) and writes back its declared ``state_variable`` outputs (``water_outflux``, ``mstruct_axis``, ``sucrose_phloem``, ``amino_acids_phloem``, ``Unloading_Sucrose``, ``Unloading_Amino_Acids``, ``Unloading_Sucrose_shoot_organs``, ``Unloading_Amino_Acids_shoot_organs``, ``Export_cytokinins``, ``adventitious_to_emerge``). :param pandas.DataFrame meteo: hourly meteorological forcing, indexed by time step ``t`` (hours), with at least the columns ``PARi``, ``DOY``, ``hour``, ``air_temperature``, ``ambient_CO2``, ``humidity``, @@ -497,7 +500,8 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, # TODO GB : Temporary inputs initialization at Component's prescribed value self.cnw_grass_root_props["Unloading_Sucrose"] = self.props["Unloading_Sucrose"][1] self.cnw_grass_root_props["Unloading_Amino_Acids"] = self.props["Unloading_Amino_Acids"][1] - self.g.get_vertex_property(2)['xylem']['water_potential'] = self.props["xylem_water_potential_collar"][1] + self.g.get_vertex_property(2)['xylem']['root_xylem_water_potential'] = self.props["root_xylem_water_potential"][1] + self.g.get_vertex_property(2)['xylem']['shoot_root_xylem_conductance'] = self.props["shoot_root_xylem_conductance"][1] if self.synchronize_adventitious_emergence: # Specific initialization for already emerged leaves at the begining of the simulation @@ -532,7 +536,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.g.get_vertex_property(2)['phloem']['amino_acids'] = self.props["amino_acids_phloem"][1] self.g.get_vertex_property(2)['phloem']['Unloading_Sucrose_shoot_organs'] = 30. self.g.get_vertex_property(2)['phloem']['Unloading_Amino_Acids_shoot_organs'] = 1. - self.g.get_vertex_property(2)['xylem']['water_outflux'] = 0. + self.g.get_vertex_property(2)['xylem']['water_potential'] = self.props["xylem_water_potential"][1] self.sync_shoot_outputs_with_root_mtg() @@ -541,12 +545,14 @@ def sync_shoot_inputs_with_shoot_mtg(self): for name in self.inputs: if name == "Unloading_Sucrose_phloem": self.cnw_grass_root_props["Unloading_Sucrose"] = self.props[name][1] / self.props['mstruct'][1] + # print(self.cnw_grass_root_props["Unloading_Sucrose"]) elif name == "Unloading_Amino_Acids_phloem": self.cnw_grass_root_props["Unloading_Amino_Acids"] = self.props[name][1] / self.props['mstruct'][1] - elif name == 'xylem_water_potential_collar': - self.g.get_vertex_property(2)['xylem']['water_potential'] = self.props[name][1] + elif name in ('root_xylem_water_potential', 'shoot_root_xylem_conductance'): + self.g.get_vertex_property(2)['xylem'][name] = self.props[name][1] + # print("received", name, self.g.get_vertex_property(2)['xylem'][name]) else: self.cnw_grass_root_props[name] = self.props[name][1] @@ -555,8 +561,9 @@ def sync_shoot_outputs_with_root_mtg(self): # Link this specific data structure to self for variables exchange, only for outputs that will be read by other models here. # Note : here eval is necessary to ensure intended lambda function definition for name in self.state_variables: - if name == "water_outflux": - self.props[name].update({1: self.g.get_vertex_property(2)['xylem']['water_outflux']}) + if name == "xylem_water_potential": + self.props[name].update({1: self.g.get_vertex_property(2)['xylem']['water_potential']}) + # print("sent potential", self.props[name][1]) elif name == "mstruct_axis": self.props[name].update({1: self.g.get_vertex_property(2)['mstruct']}) diff --git a/src/openalea/cnwgrass/integration/hydraulics_facade.py b/src/openalea/cnwgrass/integration/hydraulics_facade.py index b006932..42ae327 100644 --- a/src/openalea/cnwgrass/integration/hydraulics_facade.py +++ b/src/openalea/cnwgrass/integration/hydraulics_facade.py @@ -233,7 +233,7 @@ def _initialize_model(self): 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"] + 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 = {} From eb81bdd6d20bacba57ef2173a2c0cf42826f7f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 3 Sep 2026 13:56:48 +0200 Subject: [PATCH 15/21] outputs cleaning --- src/openalea/cnwgrass/hydraulics/simulation.py | 2 +- src/openalea/cnwgrass/morphogenesis/parameters.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index 68039af..0059733 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -582,7 +582,7 @@ def run(self): 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) + # 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) self.nfev_total += sol.nfev diff --git a/src/openalea/cnwgrass/morphogenesis/parameters.py b/src/openalea/cnwgrass/morphogenesis/parameters.py index 27e43a4..3270732 100644 --- a/src/openalea/cnwgrass/morphogenesis/parameters.py +++ b/src/openalea/cnwgrass/morphogenesis/parameters.py @@ -114,9 +114,9 @@ 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 + 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. From 2cc0c23daa9f1609bcff4b93bb24c11e52084f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Tue, 22 Sep 2026 07:31:46 +0200 Subject: [PATCH 16/21] changed shoot-root water coupling boundary condition and associated shoot psi_ref and GAMMA parameters --- src/openalea/cnwgrass/hydraulics/model.py | 36 ++++++--- .../cnwgrass/hydraulics/parameters.py | 33 +++++---- .../cnwgrass/hydraulics/simulation.py | 31 ++++---- .../cnwgrass/integration/component.py | 25 +++---- src/openalea/cnwgrass/morphogenesis/model.py | 62 ++++++++-------- .../cnwgrass/morphogenesis/parameters.py | 74 ++++++++++--------- 6 files changed, 138 insertions(+), 123 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/model.py b/src/openalea/cnwgrass/hydraulics/model.py index 3bf4e0c..bffe38a 100644 --- a/src/openalea/cnwgrass/hydraulics/model.py +++ b/src/openalea/cnwgrass/hydraulics/model.py @@ -248,8 +248,8 @@ def __init__(self, label='xylem', water_potential=INIT_COMPARTMENTS.water_potent # state parameters self.water_potential = water_potential #: MPa - self.root_xylem_water_potential = water_potential #: MPa first initialized for 0 gradient - self.shoot_root_xylem_conductance = 1e-2 # g.MPa-1.s-1 + self.root_to_shoot_xylem_water_flow = 0. + self.old_root_to_shoot_xylem_water_flow = 0. # integrative variables self.delta_t = 3600 #: the delta t of the simulation (in seconds) @@ -394,11 +394,18 @@ def calculate_osmotic_water_potential(fructan, sucrose, amino_acids, volume, tem sucrose = (sucrose * 1E-6) / parameters.NB_C_SUCROSE amino_acids = (amino_acids * 1E-6) / parameters.AMINO_ACIDS_N_RATIO fructan = (fructan * 1E-6) / parameters.NB_C_SUCROSE - conc_solutes = (fructan + sucrose + amino_acids) / (volume * parameters.VSTORAGE) + conc_solutes = (fructan + sucrose + amino_acids) / (volume * parameters.VSTORAGE) if volume > 0. else 0. #: Effective concentration of solutes - conc_solutes_eff = HiddenZone.PARAMETERS.Sa / (HiddenZone.PARAMETERS.Sb + exp(HiddenZone.PARAMETERS.Sc * - conc_solutes / HiddenZone.PARAMETERS.Sd)) + try: + conc_solutes_eff = HiddenZone.PARAMETERS.Sa / (HiddenZone.PARAMETERS.Sb + exp(HiddenZone.PARAMETERS.Sc * + conc_solutes / HiddenZone.PARAMETERS.Sd)) + except OverflowError: + print("DEBUG calculate_osmotic_water_potential OverflowError (HiddenZone):") + print(f" volume={volume!r}, temperature={temperature!r}") + print(f" converted sucrose={sucrose!r}, amino_acids={amino_acids!r}, fructan={fructan!r}") + print(f" conc_solutes={conc_solutes!r}, Sc/Sd*conc_solutes={HiddenZone.PARAMETERS.Sc * conc_solutes / HiddenZone.PARAMETERS.Sd!r}") + raise osmotic_water_potential = - parameters.R * temperature_K * conc_solutes_eff / parameters.RHO_WATER @@ -441,6 +448,7 @@ def calculate_resistance(hiddenzone_dimensions): """ resistance = 0.5 * Xylem.PARAMETERS.R_xylem_hz * (hiddenzone_dimensions['length'] / (hiddenzone_dimensions['width'] * hiddenzone_dimensions['thickness'])) + # resistance = 0.5 * (hiddenzone_dimensions['thickness'] / (hiddenzone_dimensions['width'] * hiddenzone_dimensions['length'])) return resistance @staticmethod @@ -566,7 +574,7 @@ def calculate_delta_turgor_water_potential(phi, turgor_water_potential, organ_vo plastic_component = (phi['x'] + phi['y'] + phi['z']) #: Plastic irreversible growth 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) + parameters.RHO_WATER * max(organ_volume, 1e-11) * parameters.VSTORAGE)) * delta_water_content - plastic_component * (max(turgor_water_potential, HiddenZone.PARAMETERS.GAMMA) - HiddenZone.PARAMETERS.GAMMA)) * elastic_component #: (MPa) return delta_turgor_water_potential @@ -796,6 +804,7 @@ def calculate_resistance(organ_dimensions): """ #: Coussement et al. (2018) resistance = 0.5 * Xylem.PARAMETERS.R_xylem_organ * organ_dimensions['length'] / (organ_dimensions['width'] * organ_dimensions['thickness']) + # resistance = 0.5 * (organ_dimensions['thickness'] / (organ_dimensions['width'] * organ_dimensions['length'])) return resistance @@ -863,11 +872,18 @@ def calculate_osmotic_water_potential(sucrose, amino_acids, volume, temperature, sucrose = (sucrose * 1E-6) / parameters.NB_C_SUCROSE amino_acids = (amino_acids * 1E-6) / parameters.AMINO_ACIDS_N_RATIO fructan = (fructan * 1E-6) / parameters.NB_C_SUCROSE - conc_solutes = (fructan + sucrose + amino_acids) / (volume * parameters.VSTORAGE) + conc_solutes = (fructan + sucrose + amino_acids) / (volume * parameters.VSTORAGE) if volume > 0. else 0. #: Effective concentration of solutes - conc_solutes_eff = PhotosyntheticOrganElement.PARAMETERS.Sa / (PhotosyntheticOrganElement.PARAMETERS.Sb + exp(PhotosyntheticOrganElement.PARAMETERS.Sc * - conc_solutes / PhotosyntheticOrganElement.PARAMETERS.Sd)) + try: + conc_solutes_eff = PhotosyntheticOrganElement.PARAMETERS.Sa / (PhotosyntheticOrganElement.PARAMETERS.Sb + exp(PhotosyntheticOrganElement.PARAMETERS.Sc * + conc_solutes / PhotosyntheticOrganElement.PARAMETERS.Sd)) + except OverflowError: + print("DEBUG calculate_osmotic_water_potential OverflowError (PhotosyntheticOrganElement):") + print(f" volume={volume!r}, temperature={temperature!r}") + print(f" converted sucrose={sucrose!r}, amino_acids={amino_acids!r}, fructan={fructan!r}") + print(f" conc_solutes={conc_solutes!r}, Sc/Sd*conc_solutes={PhotosyntheticOrganElement.PARAMETERS.Sc * conc_solutes / PhotosyntheticOrganElement.PARAMETERS.Sd!r}") + raise osmotic_water_potential = - parameters.R * temperature_K * conc_solutes_eff / parameters.RHO_WATER @@ -927,7 +943,7 @@ def calculate_delta_turgor_water_potential(volume, delta_water_content): 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) delta_turgor_water_potential = ((1 / ( - parameters.RHO_WATER * volume * parameters.VSTORAGE)) * delta_water_content - plastic_component) * elastic_component #: (MPa) + parameters.RHO_WATER * max(volume, 1e-11) * parameters.VSTORAGE)) * delta_water_content - plastic_component) * elastic_component #: (MPa) return delta_turgor_water_potential diff --git a/src/openalea/cnwgrass/hydraulics/parameters.py b/src/openalea/cnwgrass/hydraulics/parameters.py index 1f3b729..7b9f134 100644 --- a/src/openalea/cnwgrass/hydraulics/parameters.py +++ b/src/openalea/cnwgrass/hydraulics/parameters.py @@ -73,7 +73,7 @@ class AxisInitCompartments(object): """ def __init__(self): # state parameters - self.SAM_temperature = 12 #: initial temperature of shoot apical meristem (°C) + self.SAM_temperature = 12 #: initial temperature of shoot apical meristem (�C) #: The instance of class :class:`hydraulics.parameters.PhytomerInitCompartments` for current process AXIS_INIT_COMPARTMENTS = AxisInitCompartments() @@ -127,16 +127,16 @@ def __init__(self): self.phi_initial = {'x': 13E-09, 'y': 10E-09, 'z': 34E-06} #: Initial dimension-specific cell wall extensibility in relation to non-reversible dimensional changes (MPa-1 s-1) # Length - self.tend = 2160000 #: end of leaf elongation in automate growth (s at 12°c); fitted from adapted data from Fournier 2005 - self.tmax = 1473120 #: time at which leaf elongation rate is maximal in automate growth (s at 12°c); fitted from adapted data from Fournier 2005 - self.tbase = -822960 #: beginning of leaf elongation in automate growth (s at 12°c); fitted from adapted data from Fournier 2005 + self.tend = 2160000 #: end of leaf elongation in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 + self.tmax = 1473120 #: time at which leaf elongation rate is maximal in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 + self.tbase = -822960 #: beginning of leaf elongation in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 # Parameters for temperature responses - self.Temp_Tref = 12 # Arbitrary reference temperature (°C) + self.Temp_Tref = 12 # Arbitrary reference temperature (�C) self.Temp_Ea_R = 8900 # Parameter Ea/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (K) self.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) self.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) - self.Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(°C) + self.Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(�C) # Maximal leaf length self.leaf_Lmax_MAX = 0.45 #: Maximum leaf_Lmax (m) (Gauthier et al., 2021) @@ -150,7 +150,8 @@ def __init__(self): self.RATIO_MSTRUCT_DM = 0.8 #: Ratio mstruct/dry matter (dimensionless). From growth model. self.SLOPE_MASS_VOLUME = 3.23337E-06 #: Slope of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3 g-1). Found from Williams 1960, Fig 11. self.OFFSET_MASS_VOLUME = 1.82312E-13 #: Offset of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3). Found from Williams 1960, Fig 11. - self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. + # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. + self.GAMMA = 0.11 self.Sa = 260 #: (mol m-3) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential self.Sb = 0.9 #: (-) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential @@ -171,9 +172,9 @@ class HiddenZoneInitCompartments(object): def __init__(self): self.Tr = 0 #: mmol H20 m-2 s-1 self.green_area = 0 #: m2 - self.temperature = 8 #: °C - self.hiddenzone_age = 0 #: °Cd - self.leaf_pseudo_age = -1 #: °Cd + self.temperature = 8 #: �C + self.hiddenzone_age = 0 #: �Cd + self.leaf_pseudo_age = -1 #: �Cd self.amino_acids = 0.000075 #: :math:`:math:`\\mu mol N`` self.proteins = 0.0011 #: :math:`:math:`\\mu mol N`` self.sucrose = 0.000384 #: :math:`:math:`\\mu mol C`` @@ -280,14 +281,14 @@ class PhotosyntheticOrganElementInitCompartments(object): def __init__(self): # state parameters self.is_growing = None #: - - self.age = None #: °Cd - self.Wmax = None #: °Cd + self.age = None #: �Cd + self.Wmax = None #: �Cd self.amino_acids = 0 #: :math:`:math:`\\mu mol N`` self.proteins = 0 #: :math:`:math:`\\mu mol N`` self.sucrose = 0 #: :math:`:math:`\\mu mol C`` self.fructan = 0 #: :math:`:math:`\\mu mol C`` - self.temperature = 0 #: °C - self.Ts = 12 #: °C + self.temperature = 0 #: �C + self.Ts = 12 #: �C self.green_area = 1E-4 #: initial value of green_area (m2) self.Tr = 0 #: mmol H20 m-2 s-1 self.mstruct = 0 #: g @@ -395,8 +396,8 @@ class SoilParameters(object): """ def __init__(self): self.AWC = 50 # Available Water Capacity (g) - self.Soil_a = 15.906 #: Mpa - Parameter for soil water function (adapté pour sol limono-argileux profond, Grignon) - self.Soil_b = 18.902 #: % - Parameter for soil water function (adapté pour sol limono-argileux profond, Grignon) + self.Soil_a = 15.906 #: Mpa - Parameter for soil water function (adapt� pour sol limono-argileux profond, Grignon) + self.Soil_b = 18.902 #: % - Parameter for soil water function (adapt� pour sol limono-argileux profond, Grignon) #: The instance of class :class:`cnmetabolism.parameters.SoilParameters` for current process SOIL_PARAMETERS = SoilParameters() diff --git a/src/openalea/cnwgrass/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index 0059733..479c8f7 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -150,7 +150,7 @@ class Simulation(object): #: of the compartments associated to each organ (see :attr:`MODEL_COMPARTMENTS_NAMES`) ORGANS_STATE = ORGANS_STATE_PARAMETERS + MODEL_COMPARTMENTS_NAMES.get(model.Organ, []) #: the variables that we need to compute in order to compute fluxes and/or compartments values at organ scale - ORGANS_INTERMEDIATE_VARIABLES = ['water_potential', 'root_xylem_water_potential', 'shoot_root_xylem_conductance'] + ORGANS_INTERMEDIATE_VARIABLES = ['water_potential', 'root_to_shoot_xylem_water_flow', 'old_root_to_shoot_xylem_water_flow'] #: the fluxes exchanged between the compartments at organ scale ORGANS_FLUXES = [] #: the variables computed by integrating values of xylem components parameters/variables recursively @@ -351,6 +351,8 @@ def __init__(self, delta_t=1, interpolate_forcing=False, elements_forcing_delta_ self.nfev_total = 0 #: cumulative number of RHS function evaluations + self.old_root_to_shoot_xylem_water_flow = 0. + def initialize(self, population, soils): """ Initialize: @@ -577,12 +579,12 @@ def run(self): else: 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) - 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) + + # alpha_relaxation_flux = 1. + # for plant in self.population.plants: + # for axis in plant.axes: + # axis.xylem.root_to_shoot_xylem_water_flow = alpha_relaxation_flux * axis.xylem.root_to_shoot_xylem_water_flow + (1. - alpha_relaxation_flux) * self.old_root_to_shoot_xylem_water_flow + # self.old_root_to_shoot_xylem_water_flow = axis.xylem.root_to_shoot_xylem_water_flow self.nfev_total += sol.nfev @@ -1104,12 +1106,10 @@ def _calculate_shoot_derivatives(self, t, y): y_derivatives = np.zeros_like(y) - sum_organs_kr = 0. - sum_organs_kr_psi = 0. - #: Water flux with xylem and organs for plant in self.population.plants: for axis in plant.axes: + sum_organs_water_influx = 0. # WARNING, for now only loops on one axis with the use of nb_replications for phytomer in axis.phytomers: # Hidden zone hiddenzone = phytomer.hiddenzone @@ -1163,8 +1163,7 @@ def _calculate_shoot_derivatives(self, t, y): hiddenzone.water_outflow = 0 #: No water flow between hiddenzone and element # Positionned here to capture the last computation of the last loop - sum_organs_kr += hiddenzone.nb_replications / hiddenzone.resistance - sum_organs_kr_psi += hiddenzone.nb_replications * hiddenzone.water_potential / hiddenzone.resistance + sum_organs_water_influx += hiddenzone.nb_replications * hiddenzone.water_influx # Photosynthetic Organ Elements # for organ in (phytomer.lamina, phytomer.internode, phytomer.sheath): @@ -1222,8 +1221,9 @@ def _calculate_shoot_derivatives(self, t, y): #: Water fluxes with xylem element.water_influx = element.calculate_water_flux(element.water_potential, axis.xylem.water_potential, element.resistance, self.delta_t) - sum_organs_kr += element.nb_replications / element.resistance # TODO check replication for elements - sum_organs_kr_psi += element.nb_replications * element.water_potential / element.resistance + sum_organs_water_influx += element.nb_replications * element.water_influx + + axis.xylem.root_to_shoot_xylem_water_flow = sum_organs_water_influx #: compute the derivative of each compartment of element for plant in self.population.plants: @@ -1339,9 +1339,6 @@ def _calculate_shoot_derivatives(self, t, y): element.organ_volume = element.calculate_organ_volume(element.organ_dimensions) element.WC_mstruct = element.water_content / (element.water_content + element.mstruct) * 100 - self.sum_organs_kr = sum_organs_kr - self.sum_organs_kr_psi = sum_organs_kr_psi - derivatives_logger = logging.getLogger('hydraulics.derivatives') if logger.isEnabledFor(logging.DEBUG) and derivatives_logger.isEnabledFor(logging.DEBUG): self._log_compartments(t_abs, y_derivatives, Simulation.LOGGERS_NAMES['derivatives']) diff --git a/src/openalea/cnwgrass/integration/component.py b/src/openalea/cnwgrass/integration/component.py index dd2b05d..cd1e634 100644 --- a/src/openalea/cnwgrass/integration/component.py +++ b/src/openalea/cnwgrass/integration/component.py @@ -55,15 +55,12 @@ class CNW_Grass(Model): mstruct: float = declare(default=0., unit="g", unit_comment="", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="input", by="root_carbon", state_variable_type="extensive", edit_by="user") - root_xylem_water_potential: float = declare(default=-0.1, unit="MPa", unit_comment="", - min_value="", max_value="", description="", value_comment="", references="", DOI="", - variable_type="input", by="root_water", state_variable_type="extensive", edit_by="user") - shoot_root_xylem_conductance: float = declare(default=1e-2, unit="g.MPa-1.s-1", unit_comment="of water", + xylem_water_potential: float = declare(default=-0.6, unit="MPa", unit_comment="input shoot xylem uniform total water potential (hydrostatic + osmotic)", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="input", by="root_water", state_variable_type="extensive", edit_by="user") # State variables condidered as outputs to bellowground models - xylem_water_potential: float = declare(default=-0.1, unit="MPa", unit_comment="", + root_to_shoot_xylem_water_flow: float = declare(default=0., unit="g H2O . time_step-1", unit_comment="", min_value="", max_value="", description="", value_comment="", references="", DOI="", variable_type="state_variable", by="model_shoot", state_variable_type="extensive", edit_by="user") mstruct_axis: float = declare(default=0.05, unit="g", unit_comment="of axis structural mass", @@ -500,8 +497,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, # TODO GB : Temporary inputs initialization at Component's prescribed value self.cnw_grass_root_props["Unloading_Sucrose"] = self.props["Unloading_Sucrose"][1] self.cnw_grass_root_props["Unloading_Amino_Acids"] = self.props["Unloading_Amino_Acids"][1] - self.g.get_vertex_property(2)['xylem']['root_xylem_water_potential'] = self.props["root_xylem_water_potential"][1] - self.g.get_vertex_property(2)['xylem']['shoot_root_xylem_conductance'] = self.props["shoot_root_xylem_conductance"][1] + self.g.get_vertex_property(2)['xylem']['water_potential'] = self.props["xylem_water_potential"][1] if self.synchronize_adventitious_emergence: # Specific initialization for already emerged leaves at the begining of the simulation @@ -536,7 +532,7 @@ def __init__(self, root_mtg, meteo, inputs_dataframes, self.g.get_vertex_property(2)['phloem']['amino_acids'] = self.props["amino_acids_phloem"][1] self.g.get_vertex_property(2)['phloem']['Unloading_Sucrose_shoot_organs'] = 30. self.g.get_vertex_property(2)['phloem']['Unloading_Amino_Acids_shoot_organs'] = 1. - self.g.get_vertex_property(2)['xylem']['water_potential'] = self.props["xylem_water_potential"][1] + self.g.get_vertex_property(2)['xylem']['root_to_shoot_xylem_water_flow'] = self.props["root_to_shoot_xylem_water_flow"][1] self.sync_shoot_outputs_with_root_mtg() @@ -550,9 +546,8 @@ def sync_shoot_inputs_with_shoot_mtg(self): elif name == "Unloading_Amino_Acids_phloem": self.cnw_grass_root_props["Unloading_Amino_Acids"] = self.props[name][1] / self.props['mstruct'][1] - elif name in ('root_xylem_water_potential', 'shoot_root_xylem_conductance'): - self.g.get_vertex_property(2)['xylem'][name] = self.props[name][1] - # print("received", name, self.g.get_vertex_property(2)['xylem'][name]) + elif name == "xylem_water_potential": + self.g.get_vertex_property(2)['xylem']["water_potential"] = self.props[name][1] else: self.cnw_grass_root_props[name] = self.props[name][1] @@ -561,9 +556,8 @@ def sync_shoot_outputs_with_root_mtg(self): # Link this specific data structure to self for variables exchange, only for outputs that will be read by other models here. # Note : here eval is necessary to ensure intended lambda function definition for name in self.state_variables: - if name == "xylem_water_potential": - self.props[name].update({1: self.g.get_vertex_property(2)['xylem']['water_potential']}) - # print("sent potential", self.props[name][1]) + if name == "root_to_shoot_xylem_water_flow": + self.props[name].update({1: self.g.get_vertex_property(2)['xylem']['root_to_shoot_xylem_water_flow']}) elif name == "mstruct_axis": self.props[name].update({1: self.g.get_vertex_property(2)['mstruct']}) @@ -740,7 +734,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}, 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): + isolated_roots = False, cnwgrass_roots = True, hydraulics = False, stomatal_model_name = 'BWB'): scenario = {} ### DIRS ### @@ -854,6 +848,7 @@ def scenario_utility(time_step_in_seconds: int = 3600, INPUTS_DIRPATH = "inputs" scenario["isolated_roots"] = isolated_roots scenario["cnwgrass_roots"] = cnwgrass_roots scenario["hydraulics"] = hydraulics + scenario["stomatal_model_name"] = stomatal_model_name return scenario diff --git a/src/openalea/cnwgrass/morphogenesis/model.py b/src/openalea/cnwgrass/morphogenesis/model.py index 960dc8e..b6f436b 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 @@ -243,9 +243,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 +317,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 +479,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 +504,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 +527,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 +548,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) @@ -688,9 +688,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 +772,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 +932,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 @@ -996,19 +996,23 @@ def calculate_ratio_DZ_postE(self, leaf_L, leaf_pseudo_age, leaf_pseudostem_leng log_L_mid = log10(self.parameters.ratio_DZ_l_mid) log_Lend = log10(self.parameters.ratio_DZ_l_end) - age_normalised = leaf_pseudo_age / self.parameters.te - log_age_normalised = log10(age_normalised) - - if log_age_normalised >= log_Lend: - return 0 - elif log_age_normalised <= log_L_init: + if leaf_pseudo_age <= 0.: return 1 else: - return min((1 - (1 + (log_Lend - log_age_normalised) / (log_Lend - log_L_mid)) * - (((log_age_normalised - log_L_init) / (log_Lend - log_L_init)) ** ( - (log_Lend - log_L_init) / (log_Lend - log_L_mid)))) * leaf_L / min(leaf_L, - leaf_pseudostem_length), - 1.) + age_normalised = leaf_pseudo_age / self.parameters.te + log_age_normalised = log10(age_normalised) + + if log_age_normalised >= log_Lend: + return 0 + elif log_age_normalised <= log_L_init: + return 1 + else: + return min((1 - (1 + (log_Lend - log_age_normalised) / (log_Lend - log_L_mid)) * + (((log_age_normalised - log_L_init) / (log_Lend - log_L_init)) ** ( + (log_Lend - log_L_init) / (log_Lend - log_L_mid)))) * leaf_L / min(leaf_L, + leaf_pseudostem_length), + 1.) + def calculate_lamina_Lmax(self, leaf_rank): diff --git a/src/openalea/cnwgrass/morphogenesis/parameters.py b/src/openalea/cnwgrass/morphogenesis/parameters.py index 3270732..67e7db4 100644 --- a/src/openalea/cnwgrass/morphogenesis/parameters.py +++ b/src/openalea/cnwgrass/morphogenesis/parameters.py @@ -17,35 +17,35 @@ def __init__(self): # --- SAM # ------------------------------------------------------------------------------------------------------------------- # Parameters for temperature responses - self.Temp_Tref = 12 # Arbitrary reference temperature (°C) + self.Temp_Tref = 12 # Arbitrary reference temperature (�C) self.Temp_Ea_R = 8900 # Parameter Ea/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (K) self.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) self.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) - self.Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(°C) + self.Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(�C) # Development - self.PLASTOCHRONE = 70 / self.Temp_Tref * 24 * 3600 #76.1/12*24*3600 #: Leaf plastochron (s at 12°C) calculated from Ljutovac 2002 with primordia of 5E-5 m (76 dd) ; Malvoisin 35dd associated with init 3E-5 m + self.PLASTOCHRONE = 70 / self.Temp_Tref * 24 * 3600 #76.1/12*24*3600 #: Leaf plastochron (s at 12�C) calculated from Ljutovac 2002 with primordia of 5E-5 m (76 dd) ; Malvoisin 35dd associated with init 3E-5 m self._delta_TT_GA = None self.max_nb_leaves = 11 #: Max number of leaves per axis # ------------------------------------------------------------------------------------------------------------------- # --- Leaves # ------------------------------------------------------------------------------------------------------------------- - self.conc_sucrose_offset = 240 #: sucrose concentration under which no leaf and internode growth occurs (µmol of C g-1 mstruct) - same for root growth, see growth + self.conc_sucrose_offset = 240 #: sucrose concentration under which no leaf and internode growth occurs (�mol of C g-1 mstruct) - same for root growth, see growth # Exponential elongation self.RERmax_Ljutovac_fit = {2: 2.55e-06, 3: 1.85e-06, 4: 1.7e-06, 5: 1.6e-06, 6: 1.5e-06, 7: 1.5e-06, 8: 1.5e-06, 9: 1.5e-06, 10: 1.4e-06, - 11: 1.4e-06} # Optimal RERmax (s-1 at 12°C) calibrated from Ljutovac (2002) - # { 5 : 0.00000279 , 6 : 0.00000176 , 7 : 0.00000162 , 8 : 0.00000144 , 9 : 0.00000144 , 10 : 0.00000144 , 11 : 0.00000142 } # Observed RER on data of Ljutovac 2002 RER (s-1 at 12°C) - #RERmax = {2: 2.1e-06, 3: 2.5e-06, 4: 2.68e-06, 5: 2.65e-06, 6: 2.65e-06, 7: 2.65e-06, 8: 2.5e-06, 9: 2.3e-06, 10: 2.2e-06, 11: 2.2e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation - # self.RERmax = {2: 2.e-06, 3: 2.33e-06, 4: 2.19e-06, 5: 2.19e-06, 6: 2.25e-06, 7: 2.15e-06, 8: 2.1e-06, 9: 1.9e-06, 10: 1.75e-06, 11: 1.7e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation + 11: 1.4e-06} # Optimal RERmax (s-1 at 12�C) calibrated from Ljutovac (2002) + # { 5 : 0.00000279 , 6 : 0.00000176 , 7 : 0.00000162 , 8 : 0.00000144 , 9 : 0.00000144 , 10 : 0.00000144 , 11 : 0.00000142 } # Observed RER on data of Ljutovac 2002 RER (s-1 at 12�C) + #RERmax = {2: 2.1e-06, 3: 2.5e-06, 4: 2.68e-06, 5: 2.65e-06, 6: 2.65e-06, 7: 2.65e-06, 8: 2.5e-06, 9: 2.3e-06, 10: 2.2e-06, 11: 2.2e-06} # RERmax (s-1 at 12�C) fitted for simulations accounting for metabolic regulation + # self.RERmax = {2: 2.e-06, 3: 2.33e-06, 4: 2.19e-06, 5: 2.19e-06, 6: 2.25e-06, 7: 2.15e-06, 8: 2.1e-06, 9: 1.9e-06, 10: 1.75e-06, 11: 1.7e-06} # RERmax (s-1 at 12�C) fitted for simulations accounting for metabolic regulation self.RERmax = {2: 1.85e-06, 3: 2.57e-06, 4: 2.33e-06, 5: 2.22e-06, 6: 2.18e-06, 7: 2.05e-06, 8: 1.9e-06, 9: 1.75e-06, 10: 1.7e-06, - 11: 1.7e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation + 11: 1.7e-06} # RERmax (s-1 at 12�C) fitted for simulations accounting for metabolic regulation - self.RER_Kc = 100 #: affinity coefficient of RER to C (µmol g-1) - self.RER_Kn = 15 #: affinity coefficient of RER to N (µmol g-1) + self.RER_Kc = 100 #: affinity coefficient of RER to C (�mol g-1) + self.RER_Kn = 15 #: affinity coefficient of RER to N (�mol g-1) # Automate elongation #- Parameters for leaf 1 and 2 @@ -53,15 +53,15 @@ def __init__(self): self.tm_init = 150 * 3600 * 24 / 12 self.tb_init = -35 * 3600 * 24 / 12 # - Following leaves - self.te = 300 * 3600 * 24 / self.Temp_Tref #: end of leaf elongation in automate growth (s at 12°c); fitted from adapted data from Fournier 2005 - self.tm = 204.6 * 3600 * 24 / self.Temp_Tref #: time at which leaf elongation rate is maximal in automate growth (s at 12°c); fitted from adapted data from Fournier 2005 - self.tb = -114.3 * 3600 * 24 / self.Temp_Tref #: beginning of leaf elongation in automate growth (s at 12°c); fitted from adapted data from Fournier 2005 + self.te = 300 * 3600 * 24 / self.Temp_Tref #: end of leaf elongation in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 + self.tm = 204.6 * 3600 * 24 / self.Temp_Tref #: time at which leaf elongation rate is maximal in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 + self.tb = -114.3 * 3600 * 24 / self.Temp_Tref #: beginning of leaf elongation in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 # NB : Previous fit on adapted data from Fournier 2005 in phyllochronic time te = 271, tm=176, tb=-25 self.leaf_Lmax_MAX = 0.45 #: Maximum leaf_Lmax (m) self.leaf_pseudo_age_Vmax = 1.2 #: Maximal regulation of leaf length after emergence by CN status (dimensionless) - self.leaf_pseudo_age_Kc = 150 #: affinity coefficient to C (µmol g-1) - self.leaf_pseudo_age_Kn = 4 #: affinity coefficient to N (µmol g-1) + self.leaf_pseudo_age_Kc = 150 #: affinity coefficient to C (�mol g-1) + self.leaf_pseudo_age_Kn = 4 #: affinity coefficient to N (�mol g-1) # Leaf maximal width #- If optimal growth used @@ -84,7 +84,7 @@ def __init__(self): self.leaf_SSLW_MIN = 5. self.leaf_SSLW_MAX = 45. self.leaf_SSLW_a = 47.50516 # g m-2 - self.leaf_SSLW_b = 2927.944 # µmol C g-1 + self.leaf_SSLW_b = 2927.944 # �mol C g-1 self.leaf_LSSW_dict = {1: 0.06, 2: 0.08, 3: 0.08, 4: 0.18, 5: 0.17, 6: 0.21, 7: 0.24, 8: 0.4, 9: 0.5, 10: 0.55, 11: 0.65} # Manip NEMA 05/06 Soissons N+ (from data of J. Bertheloot, 2004) @@ -115,10 +115,10 @@ def __init__(self): # ------------------------------------------------------------------------------------------------------------------- # 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 - 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 + # 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 + 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 + # { 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. # Because i) not enough data if we consider only up to previous leaf ligulation, ii) same exponential like period @@ -153,7 +153,7 @@ def delta_TT_GA(self): if self._delta_TT_GA is not None: return self._delta_TT_GA else: - return self.PLASTOCHRONE * 5 #: Thermal time between floral transition of SAM and Gibberelin production expressed as a function of plastochron (s at 12°C) ; Malvoisin's data give 7 plastochrons + return self.PLASTOCHRONE * 5 #: Thermal time between floral transition of SAM and Gibberelin production expressed as a function of plastochron (s at 12�C) ; Malvoisin's data give 7 plastochrons @delta_TT_GA.setter def delta_TT_GA(self, value): @@ -165,13 +165,15 @@ class ParametersHydraulics(Parameters): def __init__(self): super().__init__() self.PLASTOCHRONE = 76.1 / self.Temp_Tref * 24 * 3600 - self.RERmax = {1: 2.4E-06, 2: 2.4E-06, 3: 1.85E-06, 4: 3.E-08, 5: 3.45e-06, 6: 2.3e-06, 7: 2.2e-06, 8: 2.1e-06, 9: 2.0e-06, 10: 1.85e-06, 11: 1.76e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation + self.RERmax = {1: 2.4E-06, 2: 2.4E-06, 3: 1.85E-06, 4: 3.E-08, 5: 3.45e-06, 6: 2.3e-06, 7: 2.2e-06, 8: 2.1e-06, 9: 2.0e-06, 10: 1.85e-06, 11: 1.76e-06} # RERmax (s-1 at 12�C) fitted for simulations accounting for metabolic regulation self.n = 4 #: Shape parameter of the xylem water potential function in RER (-) - self.psi_ref = -0.185 #: Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) + # self.psi_ref = -0.185 # Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) + self.psi_ref = -1. #: Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) self.lamina_Lmax_dict = {3: 0.09896253, 4: 0.10463127, 5: 0.10867923, 6: 0.1352669, 7: 0.16634665, 8: 0.19288286, 9: 0.25080917, 10: 0.28677033, 11: 0.3} #: Maximum lamina length (m) - self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. + # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. + self.GAMMA = 0.11 def parameters_factory(hydraulics=False): @@ -227,18 +229,18 @@ def __init__(self): self.ratio_DZ = 1.0 # Default values used for RER calculation in morphogenesis - self.sucrose = 5E-6 #: µmol C - self.amino_acids = 4E-6 #: µmol N - self.fructan = 0 #: µmol C - about 10% DM + self.sucrose = 5E-6 #: �mol C + self.amino_acids = 4E-6 #: �mol N + self.fructan = 0 #: �mol C - about 10% DM self.leaf_enclosed_mstruct = 1.26E-07 #: g self.internode_enclosed_mstruct = 0 #: g self.mstruct = self.leaf_enclosed_mstruct + self.internode_enclosed_mstruct #: g self.leaf_enclosed_Nstruct = self.leaf_enclosed_mstruct * 0.005 #: g, parameter value in Growth self.internode_enclosed_Nstruct = self.internode_enclosed_mstruct * 0.0322 #: g, parameter value in Growth self.Nstruct = self.leaf_enclosed_Nstruct + self.internode_enclosed_Nstruct #: g - self.proteins = 2.6E-03 #: µmol N - about 9% N + self.proteins = 2.6E-03 #: �mol N - about 9% N self.conc_cytokinins = 150 #: AU / g mstruct - self.mean_conc_sucrose = 0 #: µmol C / g mstruct + self.mean_conc_sucrose = 0 #: �mol C / g mstruct class HiddenZoneInitHydraulics(HiddenZoneInit): @@ -267,14 +269,14 @@ def __init__(self): self.green_area = 0 #: m2 self.age = 0 #: Thermal Time self.age_teq = 0 #: s - self.max_proteins = 0 #: µmol N + self.max_proteins = 0 #: �mol N self.Nresidual = 0 #: g - self.sucrose = 0 #: µmol C - self.amino_acids = 0 #: µmol N - self.nitrates = 0 #: µmol N - self.fructan = 0 #: µmol C - self.starch = 0 #: µmol C - self.proteins = 0 #: µmol N + self.sucrose = 0 #: �mol C + self.amino_acids = 0 #: �mol N + self.nitrates = 0 #: �mol N + self.fructan = 0 #: �mol C + self.starch = 0 #: �mol C + self.proteins = 0 #: �mol N self.mstruct = 0 #: g self.senesced_mstruct = 0 #: g self.max_mstruct = 0 #: g From 8029376c5aaa172807f8b28afbabc1462b8daeb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Tue, 22 Sep 2026 07:40:18 +0200 Subject: [PATCH 17/21] fixed wrong special characters --- src/openalea/cnwgrass/cnmetabolism/model.py | 692 +++++++++--------- .../cnwgrass/cnmetabolism/simulation.py | 34 +- src/openalea/cnwgrass/gasexchange/model.py | 112 +-- .../cnwgrass/gasexchange/simulation.py | 6 +- src/openalea/cnwgrass/growth/simulation.py | 6 +- src/openalea/cnwgrass/hydraulics/model.py | 36 +- .../cnwgrass/hydraulics/parameters.py | 30 +- .../cnwgrass/hydraulics/postprocessing.py | 12 +- .../integration/cnmetabolism_facade.py | 8 +- .../cnwgrass/integration/senescence_facade.py | 2 +- src/openalea/cnwgrass/morphogenesis/model.py | 36 +- .../cnwgrass/morphogenesis/parameters.py | 68 +- .../cnwgrass/morphogenesis/simulation.py | 2 +- 13 files changed, 522 insertions(+), 522 deletions(-) diff --git a/src/openalea/cnwgrass/cnmetabolism/model.py b/src/openalea/cnwgrass/cnmetabolism/model.py index 7117f39..ed34eb8 100644 --- a/src/openalea/cnwgrass/cnmetabolism/model.py +++ b/src/openalea/cnwgrass/cnmetabolism/model.py @@ -87,9 +87,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 @@ -102,9 +102,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 @@ -166,7 +166,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. @@ -216,10 +216,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 @@ -276,17 +276,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 @@ -303,7 +303,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)) @@ -323,9 +323,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 @@ -339,13 +339,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) * \ @@ -355,10 +355,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) * \ @@ -369,10 +369,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 @@ -381,9 +381,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 @@ -405,7 +405,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 @@ -417,26 +417,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): @@ -451,11 +451,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 @@ -463,15 +463,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) @@ -481,15 +481,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) @@ -498,37 +498,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 """ @@ -542,28 +542,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) / @@ -576,13 +576,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 @@ -590,12 +590,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 @@ -603,10 +603,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 @@ -614,10 +614,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 @@ -636,8 +636,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 @@ -647,7 +647,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 @@ -671,7 +671,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 @@ -707,21 +707,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. @@ -733,7 +733,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 @@ -754,7 +754,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)) @@ -774,9 +774,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 @@ -786,15 +786,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 @@ -806,15 +806,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 @@ -829,13 +829,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: @@ -849,10 +849,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 @@ -861,11 +861,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 @@ -874,9 +874,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 @@ -901,9 +901,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 @@ -911,25 +911,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) @@ -941,10 +941,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 @@ -964,26 +964,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))) @@ -996,11 +996,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 @@ -1012,11 +1012,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 @@ -1027,30 +1027,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 @@ -1061,19 +1061,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))) * @@ -1084,51 +1084,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: @@ -1140,8 +1140,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) @@ -1177,12 +1177,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 @@ -1192,11 +1192,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 @@ -1206,12 +1206,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 @@ -1256,7 +1256,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 @@ -1343,9 +1343,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 @@ -1357,48 +1357,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): @@ -1413,12 +1413,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 @@ -1437,11 +1437,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 """ if Loading_Sucrose <= 0: @@ -1458,11 +1458,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 @@ -1470,13 +1470,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 """ if triosesP <= 0: @@ -1487,25 +1487,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 """ if triosesP <= 0: @@ -1516,44 +1516,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 @@ -1561,28 +1561,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) / @@ -1592,14 +1592,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: @@ -1610,14 +1610,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: @@ -1627,14 +1627,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 """ if nitrates <= 0 or triosesP <= 0: @@ -1647,13 +1647,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) / @@ -1662,14 +1662,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) @@ -1682,43 +1682,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 @@ -1761,12 +1761,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 @@ -1776,10 +1776,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) @@ -1787,14 +1787,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 @@ -1802,10 +1802,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) @@ -1813,10 +1813,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 @@ -1825,13 +1825,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) @@ -1839,10 +1839,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) @@ -1914,8 +1914,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 (%) """ @@ -1929,15 +1929,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 @@ -1958,9 +1958,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 @@ -1975,9 +1975,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)) @@ -1989,7 +1989,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 @@ -2000,12 +2000,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 bc2eac7..2588289 100644 --- a/src/openalea/cnwgrass/cnmetabolism/simulation.py +++ b/src/openalea/cnwgrass/cnmetabolism/simulation.py @@ -61,17 +61,17 @@ class Simulation(object): This model must define a class implementing these functions: * R_Nnit_upt(U_Nnit, sucrose): Nitrate uptake respiration. * Parameters: - - `U_Nnit` (:class:`float`) - uptake of N nitrates (�mol` N) - - `sucrose` (:class:`float`) - amount of C sucrose in organ (�mol` C) - * Returns: _R_Nnit_upt (�mol` C respired) + - `U_Nnit` (:class:`float`) - uptake of N nitrates (µmol N) + - `sucrose` (:class:`float`) - amount of C sucrose in organ (µmol C) + * Returns: _R_Nnit_upt (µmol C respired) * Returns Type: :class:`float` * R_phloem(sucrose_loading, sucrose, mstruct): Phloem loading respiration * Parameters: - - `sucrose_loading` (:class:`float`) - Loading flux from the C substrate pool to phloem (�mol` C g-1 mstruct) - - `sucrose` (:class:`float`) - amount of C sucrose in organ (�mol` C) + - `sucrose_loading` (:class:`float`) - Loading flux from the C substrate pool to phloem (µmol C g-1 mstruct) + - `sucrose` (:class:`float`) - amount of C sucrose in organ (µmol C) - `mstruct` (:class:`float`) - structural dry mass of organ (g) - * Returns: _R_phloem (�mol` C respired) + * Returns: _R_phloem (µmol C respired) * Returns Type: :class:`float` * R_Nnit_red(s_amino_acids, sucrose, mstruct, root=False): Nitrate reduction-linked respiration @@ -79,30 +79,30 @@ class Simulation(object): and reducing power obtained directly from photosynthesis (rather than C substrate) * Parameters: - - `s_amino_acids` (:class:`float`) - consumption of N for the synthesis of amino acids (�mol` N g-1 mstruct) + - `s_amino_acids` (:class:`float`) - consumption of N for the synthesis of amino acids (µmol N g-1 mstruct) (in the present version, this is used to approximate nitrate reduction needed in the original model of Thornley and Cannell, 2000) - - `sucrose` (:class:`float`) - amount of C sucrose in organ (�mol` C) + - `sucrose` (:class:`float`) - amount of C sucrose in organ (µmol C) - `mstruct` (:class:`float`) - structural dry mass of organ (g) - `root` (:class:`bool`) - specifies if the nitrate reduction-linked respiration is computed for shoot (False) or root (True) tissues. - * Returns: _R_Nnit_upt (�mol` C respired) + * Returns: _R_Nnit_upt (µmol C respired) * Returns Type: :class:`float` * R_residual(sucrose, mstruct, Ntot, delta_t, Ts): Residual maintenance respiration (cost from protein turn-over, cell ion gradients, futile cycles...) * Parameters: - - `sucrose` (:class:`float`) - amount of C sucrose (�mol` C) + - `sucrose` (:class:`float`) - amount of C sucrose (µmol C) - `mstruct` (:class:`float`) - structural dry mass of organ (g) - - `Ntot` (:class:`float`) - total N in organ (�mol` N) + - `Ntot` (:class:`float`) - total N in organ (µmol N) - `delta_t` (:class:`float`) - timestep (s) - - `Ts` (:class:`float`) - organ temperature (�C) - * Returns: _R_residual (�mol` C respired) + - `Ts` (:class:`float`) - organ temperature (°C) + * Returns: _R_residual (µmol C respired) * Returns Type: :class:`float` * R_grain_growth(mstruct_growth, starch_filling, mstruct): Grain growth respiration * Parameters: - - `mstruct_growth` (:class:`float`) - gross growth of grain structure (�mol` C added in grain structure) - - `starch_filling` (:class:`float`) - gross growth of grain starch (�mol` C added in grain starch g-1 mstruct) + - `mstruct_growth` (:class:`float`) - gross growth of grain structure (µmol C added in grain structure) + - `starch_filling` (:class:`float`) - gross growth of grain starch (µmol C added in grain starch g-1 mstruct) - `mstruct` (:class:`float`) - structural dry mass of organ (g) - * Returns: R_grain_growth (�mol` C respired) + * Returns: R_grain_growth (µmol C respired) * Returns Type: :class:`float` :param int delta_t: the delta t of the simulation (in seconds) ; default is `1`. @@ -456,7 +456,7 @@ def initialize(self, population, soils, Tsoil=12): :param model.Population population: a population of plants. :param dict soils: the soil associated to each axis. `soils` must be a dictionary with the same structure as :attr:`soils` - :param float Tsoil: soil temperature (�C) + :param float Tsoil: soil temperature (°C) """ logger = logging.getLogger(__name__) diff --git a/src/openalea/cnwgrass/gasexchange/model.py b/src/openalea/cnwgrass/gasexchange/model.py index a8db9c1..4c7732d 100644 --- a/src/openalea/cnwgrass/gasexchange/model.py +++ b/src/openalea/cnwgrass/gasexchange/model.py @@ -27,7 +27,7 @@ def organ_temperature(w, z, Zh, Ur, PAR, gsw, Ta, Ts, RH, organ_name): :param float z: organ height from soil (m) :param float Zh: canopy height (m) :param float Ur: wind speed (m s-1) at the reference height (zr), e.g. top of the canopy + 2m (in the case of wheat, Ur can be approximated as the wind speed at 2m from soil) - :param float PAR: absorbed PAR (µmol m-2 s-1) + :param float PAR: absorbed PAR (µmol m-2 s-1) :param float gsw: stomatal conductance to water vapour (mol m-2 s-1) :param float Ta: air temperature (degree C) :param float Ts: organ temperature (degree C). Ts = Ta at the first iteration of the numeric resolution @@ -97,17 +97,17 @@ def stomatal_conductance_BWB(Ag, An, surfacic_nitrogen, ambient_CO2, RH): """ Ball, Woodrow, and Berry model of stomatal conductance (1987) - :param float Ag: gross assimilation rate (µmol m-2 s-1) - :param float An: net assimilation rate (µmol m-2 s-1) + :param float Ag: gross assimilation rate (µmol m-2 s-1) + :param float An: net assimilation rate (µmol m-2 s-1) :param float surfacic_nitrogen: surfacic nitrogen content(g m-2) including or not structural nitrogen depending on parameter.MODEL_VERSION - :param float ambient_CO2: Air CO2 (µmol mol-1) + :param float ambient_CO2: Air CO2 (µmol mol-1) :param float RH: Relative humidity (decimal fraction) :return: gsw (mol m-2 s-1) :rtype: float """ - Cs = ambient_CO2 - An * (parameters.K_Cs / parameters.GB) #: CO2 concentration at organ surface (µmol mol-1 or Pa). From Prieto et al. (2012). GB in mol m-2 s-1 + Cs = ambient_CO2 - An * (parameters.K_Cs / parameters.GB) #: CO2 concentration at organ surface (µmol mol-1 or Pa). From Prieto et al. (2012). GB in mol m-2 s-1 m = parameters.PARAM_N['delta1'] * surfacic_nitrogen ** parameters.PARAM_N['delta2'] #: Scaling factor dependence to surfacic_nitrogen (dimensionless). This function is maintained # although I'm not convinced that it should be taken into account gsw = (parameters.GSMIN + m * ((Ag * RH) / Cs)) #: Stomatal conductance to water vapour (mol m-2 s-1), from Braune et al. (2009), Muller et al. (2005): using Ag rather than An. @@ -118,10 +118,10 @@ def stomatal_conductance_Leuning(Ag, An, Ta, ambient_CO2, RH): """ Leuning model of stomatal conductance to water (1995) - :param float Ag: gross assimilation rate (µmol m-2 s-1) - :param float An: net assimilation rate (µmol m-2 s-1) + :param float Ag: gross assimilation rate (µmol m-2 s-1) + :param float An: net assimilation rate (µmol m-2 s-1) :param float Ta: air temperature (degree C) - :param float ambient_CO2: Air CO2 (µmol mol-1) + :param float ambient_CO2: Air CO2 (µmol mol-1) :param float RH: Relative humidity (decimal fraction) :return: gsw (mol m-2 s-1) @@ -132,7 +132,7 @@ def stomatal_conductance_Leuning(Ag, An, Ta, ambient_CO2, RH): V = RH * es_Ta #: Vapour pressure of the air (kPa) VPDa = es_Ta - V fw = 1 / (1 + VPDa / parameters.D0) - Cs = ambient_CO2 - An * (parameters.K_Cs / parameters.GB) #: CO2 concentration at organ surface (µmol mol-1 or Pa). From Prieto et al. (2012). GB in mol m-2 s-1 + Cs = ambient_CO2 - An * (parameters.K_Cs / parameters.GB) #: CO2 concentration at organ surface (µmol mol-1 or Pa). From Prieto et al. (2012). GB in mol m-2 s-1 gamma = parameters.GAMMA0 * (1 + parameters.GAMMA1 * (Ta - parameters.T_ref) + parameters.GAMMA2 * ((Ta - parameters.T_ref) * (Ta - parameters.T_ref))) gsw = parameters.GSMIN + (1.6 * parameters.m * Ag * fw) / (Cs - gamma) @@ -143,10 +143,10 @@ def stomatal_conductance_Tuzet(Ag, An, Ta, ambient_CO2, water_potential): """ Tuzet model of stomatal conductance to water (2003) - :param float Ag: gross assimilation rate (µmol m-2 s-1) - :param float An: net assimilation rate (µmol m-2 s-1) + :param float Ag: gross assimilation rate (µmol m-2 s-1) + :param float An: net assimilation rate (µmol m-2 s-1) :param float Ta: air temperature (degree C) - :param float ambient_CO2: Air CO2 (µmol mol-1) + :param float ambient_CO2: Air CO2 (µmol mol-1) :param float water_potential: water potential of the organ (Mpa) :return: gsw (mol m-2 s-1) @@ -154,7 +154,7 @@ def stomatal_conductance_Tuzet(Ag, An, Ta, ambient_CO2, water_potential): """ fw = 1 / (1 + (water_potential / parameters.water_potential_ref) ** parameters.n) - Cs = ambient_CO2 - An * (parameters.K_Cs / parameters.GB) #: CO2 concentration at organ surface (µmol mol-1 or Pa). From Prieto et al. (2012). GB in mol m-2 s-1 + Cs = ambient_CO2 - An * (parameters.K_Cs / parameters.GB) #: CO2 concentration at organ surface (µmol mol-1 or Pa). From Prieto et al. (2012). GB in mol m-2 s-1 gamma = parameters.GAMMA0 * (1 + parameters.GAMMA1 * (Ta - parameters.T_ref) + parameters.GAMMA2 * ((Ta - parameters.T_ref) * (Ta - parameters.T_ref))) gsw = parameters.GSMIN + (1.6 * parameters.m * Ag * fw) / (Cs - gamma) @@ -165,10 +165,10 @@ def stomatal_conductance_hydraulics(Ag, An, Ta, ambient_CO2, RH, water_potential """ Model of stomatal conductance to water coupling Tuzet and Leuning - :param float Ag: gross assimilation rate (µmol m-2 s-1) - :param float An: net assimilation rate (µmol m-2 s-1) + :param float Ag: gross assimilation rate (µmol m-2 s-1) + :param float An: net assimilation rate (µmol m-2 s-1) :param float Ta: air temperature (degree C) - :param float ambient_CO2: Air CO2 (µmol mol-1) + :param float ambient_CO2: Air CO2 (µmol mol-1) :param float RH: Relative humidity (decimal fraction) :param float water_potential: water potential of the organ (Mpa) @@ -182,7 +182,7 @@ def stomatal_conductance_hydraulics(Ag, An, Ta, ambient_CO2, RH, water_potential VPDa = es_Ta - V fvpd = 1 / (1 + VPDa / parameters.D0) - Cs = ambient_CO2 - An * (parameters.K_Cs / parameters.GB) #: CO2 concentration at organ surface (µmol mol-1 or Pa). From Prieto et al. (2012). GB in mol m-2 s-1 + Cs = ambient_CO2 - An * (parameters.K_Cs / parameters.GB) #: CO2 concentration at organ surface (µmol mol-1 or Pa). From Prieto et al. (2012). GB in mol m-2 s-1 gamma = parameters.GAMMA0 * (1 + parameters.GAMMA1 * (Ta - parameters.T_ref) + parameters.GAMMA2 * ((Ta - parameters.T_ref) * (Ta - parameters.T_ref))) gsw = parameters.GSMIN + (1.6 * parameters.m * Ag * fvpd * fpsi) / (Cs - gamma) @@ -201,15 +201,15 @@ def calculate_Ci(ambient_CO2, An, gsw): """ Calculates the internal CO2 concentration (Ci) - :param float ambient_CO2: air CO2 (µmol mol-1) - :param float An: net assimilation rate of CO2 (µmol m-2 s-1) + :param float ambient_CO2: air CO2 (µmol mol-1) + :param float An: net assimilation rate of CO2 (µmol m-2 s-1) :param float gsw: stomatal conductance to water vapour (mol m-2 s-1) - :return: Ci (µmol mol-1) + :return: Ci (µmol mol-1) :rtype: float """ - Ci = ambient_CO2 - An * ((parameters.gsw_gs_CO2 / gsw) + (parameters.Ci_A / parameters.GB)) #: Intercellular concentration of CO2 (µmol mol-1) - # gsw and GB in mol m-2 s-1 so that (An * ((1.6/gs) + (1.37/parameters.GB)) is thus in µmol mol-1 as ambient_CO2 + Ci = ambient_CO2 - An * ((parameters.gsw_gs_CO2 / gsw) + (parameters.Ci_A / parameters.GB)) #: Intercellular concentration of CO2 (µmol mol-1) + # gsw and GB in mol m-2 s-1 so that (An * ((1.6/gs) + (1.37/parameters.GB)) is thus in µmol mol-1 as ambient_CO2 return Ci @@ -248,7 +248,7 @@ def _inhibition_by_NSC(NSC): """ Calculates the relative diminution of Ag due to inhibition by NSC. Adapted from Azcon-Bieto 1983 - :param float NSC: Surfacic content of water-soluble carbohydrates (µmol C m-2) + :param float NSC: Surfacic content of water-soluble carbohydrates (µmol C m-2) :return: Relative diminution (dimensionless) :rtype: float @@ -265,14 +265,14 @@ def calculate_photosynthesis(PAR, surfacic_nitrogen, NSC_Retroinhibition, surfac Computes photosynthesis rate following Farquhar's model with regulation by organ temperature and nitrogen content. In this version, most of the parameters are derived from Braune et al. (2009) on barley and Evers et al. (2010) for N dependencies. - :param float PAR: PAR absorbed (µmol m-2 s-1) + :param float PAR: PAR absorbed (µmol m-2 s-1) :param float surfacic_nitrogen: surfacic nitrogen content(g m-2) including or not structural nitrogen depending on parameter.MODEL_VERSION :param bool NSC_Retroinhibition: if True, Ag is inhibited by surfacic NSC (Non-Structural Carbohydrates). - :param float surfacic_NSC: surfacic content of NSC (Non-Structural Carbohydrates) (µmol C m-2). + :param float surfacic_NSC: surfacic content of NSC (Non-Structural Carbohydrates) (µmol C m-2). :param float Ts: organ temperature (degree C) - :param float Ci: internal CO2 (µmol mol-1), Ci = 0.7*CO2air for the first iteration + :param float Ci: internal CO2 (µmol mol-1), Ci = 0.7*CO2air for the first iteration - :return: Ag (µmol m-2 s-1), An (µmol m-2 s-1), Rd (µmol m-2 s-1) + :return: Ag (µmol m-2 s-1), An (µmol m-2 s-1), Rd (µmol m-2 s-1) :rtype: (float, float, float) """ @@ -284,34 +284,34 @@ def calculate_photosynthesis(PAR, surfacic_nitrogen, NSC_Retroinhibition, surfac #: RuBisCO-limited carboxylation rate Sna_Vcmax25 = parameters.PARAM_N['S_surfacic_nitrogen']['Vc_max25'] surfacic_nitrogen_min_Vcmax25 = parameters.PARAM_N['surfacic_nitrogen_min']['Vc_max25'] - Vc_max25 = Sna_Vcmax25 * (surfacic_nitrogen - surfacic_nitrogen_min_Vcmax25) #: Relation between Vc_max25 and surfacic_nonstructural_nitrogen (µmol m-2 s-1) - Vc_max = _f_temperature('Vc_max', Vc_max25, Ts) #: Relation between Vc_max and temperature (µmol m-2 s-1) - Ac = (Vc_max * (Ci - Gamma)) / (Ci + Kc * (1 + parameters.O2 / Ko)) #: Rate of assimilation under Vc_max limitation (µmol m-2 s-1) + Vc_max25 = Sna_Vcmax25 * (surfacic_nitrogen - surfacic_nitrogen_min_Vcmax25) #: Relation between Vc_max25 and surfacic_nonstructural_nitrogen (µmol m-2 s-1) + Vc_max = _f_temperature('Vc_max', Vc_max25, Ts) #: Relation between Vc_max and temperature (µmol m-2 s-1) + Ac = (Vc_max * (Ci - Gamma)) / (Ci + Kc * (1 + parameters.O2 / Ko)) #: Rate of assimilation under Vc_max limitation (µmol m-2 s-1) #: RuBP regeneration-limited carboxylation rate via electron transport ALPHA = parameters.PARAM_N['S_surfacic_nitrogen']['alpha'] * surfacic_nitrogen + parameters.PARAM_N['beta'] #: Relation between ALPHA and surfacic_nitrogen (mol e- mol-1 photon) Sna_Jmax25 = parameters.PARAM_N['S_surfacic_nitrogen']['Jmax25'] surfacic_nitrogen_min_Jmax25 = parameters.PARAM_N['surfacic_nitrogen_min']['Jmax25'] - Jmax25 = Sna_Jmax25 * (surfacic_nitrogen - surfacic_nitrogen_min_Jmax25) #: Relation between Jmax25 and surfacic_nitrogen (µmol m-2 s-1) - Jmax = _f_temperature('Jmax', Jmax25, Ts) #: Relation between Jmax and temperature (µmol m-2 s-1) + Jmax25 = Sna_Jmax25 * (surfacic_nitrogen - surfacic_nitrogen_min_Jmax25) #: Relation between Jmax25 and surfacic_nitrogen (µmol m-2 s-1) + Jmax = _f_temperature('Jmax', Jmax25, Ts) #: Relation between Jmax and temperature (µmol m-2 s-1) J = ((Jmax + ALPHA * PAR) - sqrt((Jmax + ALPHA * PAR) ** parameters.J_expo - parameters.J_A * parameters.THETA * ALPHA * PAR * Jmax)) / ( - parameters.J_B * parameters.THETA) #: Electron transport rate (Muller et al. (2005), Evers et al. (2010)) (µmol m-2 s-1) - Aj = (J * (Ci - Gamma)) / (parameters.Aj_A * Ci + parameters.Aj_B * Gamma) #: Rate of assimilation under RuBP regeneration limitation (µmol m-2 s-1) + parameters.J_B * parameters.THETA) #: Electron transport rate (Muller et al. (2005), Evers et al. (2010)) (µmol m-2 s-1) + Aj = (J * (Ci - Gamma)) / (parameters.Aj_A * Ci + parameters.Aj_B * Gamma) #: Rate of assimilation under RuBP regeneration limitation (µmol m-2 s-1) #: Triose phosphate utilisation-limited carboxylation rate --- NOT USED, calculated just for information Sna_TPU25 = parameters.PARAM_N['S_surfacic_nitrogen']['TPU25'] surfacic_nitrogen_min_TPU25 = parameters.PARAM_N['surfacic_nitrogen_min']['TPU25'] - TPU25 = Sna_TPU25 * (surfacic_nitrogen - surfacic_nitrogen_min_TPU25) #: Relation between TPU25 and surfacic_nitrogen (µmol m-2 s-1) - TPU = _f_temperature('TPU', TPU25, Ts) #: Relation between TPU and temperature (µmol m-2 s-1) - Vomax = (Vc_max * Ko * Gamma) / (parameters.Vomax_A * Kc * parameters.O2) #: Maximum rate of Vo (µmol m-2 s-1) (µmol m-2 s-1) - Vo = (Vomax * parameters.O2) / (parameters.O2 + Ko * (1 + Ci / Kc)) #: Rate of oxygenation of RuBP (µmol m-2 s-1) - Ap = (1 - Gamma / Ci) * (parameters.Ap_A * TPU + Vo) #: Rate of assimilation under TPU limitation (µmol m-2 s-1). + TPU25 = Sna_TPU25 * (surfacic_nitrogen - surfacic_nitrogen_min_TPU25) #: Relation between TPU25 and surfacic_nitrogen (µmol m-2 s-1) + TPU = _f_temperature('TPU', TPU25, Ts) #: Relation between TPU and temperature (µmol m-2 s-1) + Vomax = (Vc_max * Ko * Gamma) / (parameters.Vomax_A * Kc * parameters.O2) #: Maximum rate of Vo (µmol m-2 s-1) (µmol m-2 s-1) + Vo = (Vomax * parameters.O2) / (parameters.O2 + Ko * (1 + Ci / Kc)) #: Rate of oxygenation of RuBP (µmol m-2 s-1) + Ap = (1 - Gamma / Ci) * (parameters.Ap_A * TPU + Vo) #: Rate of assimilation under TPU limitation (µmol m-2 s-1). # I think there was a mistake in the paper of Braune et al. (2009) where they wrote Ap = (1-Gamma/Ci)*(3*TPU) + Vo # A more recent expression of Ap was given by S. v Caemmerer in her book (2000): AP = (3TPU * (Ci-Gamma))/(Ci-(1+3alpha)*Gamma), # where 0 < alpha > 1 is the fraction of glycolate carbon not returned to the chloroplast, but I couldn't find any estimation of alpha for wheat - #: Gross assimilation rate (µmol m-2 s-1) + #: Gross assimilation rate (µmol m-2 s-1) if NSC_Retroinhibition: Ag = min(Ac, Aj) * (1 - _inhibition_by_NSC(surfacic_NSC)) else: @@ -319,11 +319,11 @@ def calculate_photosynthesis(PAR, surfacic_nitrogen, NSC_Retroinhibition, surfac #: Mitochondrial respiration rate of organ in light Rd (processes other than photorespiration) Rdark25 = parameters.PARAM_N['S_surfacic_nitrogen']['Rdark25'] * (surfacic_nitrogen - parameters.PARAM_N['surfacic_nitrogen_min'][ - 'Rdark25']) #: Relation between Rdark25 (respiration in obscurity at 25 degree C) and surfacic_nitrogen (µmol m-2 s-1) - Rdark = _f_temperature('Rdark', Rdark25, Ts) #: Relation between Rdark and temperature (µmol m-2 s-1) - Rd = Rdark * (parameters.Rd_A + (1 - parameters.Rd_A) * parameters.Rd_B ** (PAR / parameters.Rd_C)) # Found in Muller et al. (2005), eq. 19 (µmol m-2 s-1) + 'Rdark25']) #: Relation between Rdark25 (respiration in obscurity at 25 degree C) and surfacic_nitrogen (µmol m-2 s-1) + Rdark = _f_temperature('Rdark', Rdark25, Ts) #: Relation between Rdark and temperature (µmol m-2 s-1) + Rd = Rdark * (parameters.Rd_A + (1 - parameters.Rd_A) * parameters.Rd_B ** (PAR / parameters.Rd_C)) # Found in Muller et al. (2005), eq. 19 (µmol m-2 s-1) - #: Net C assimilation (µmol m-2 s-1) + #: Net C assimilation (µmol m-2 s-1) if Ag <= 0: # Occurs when Ci is lower than Gamma or when (surfacic_nitrogen - surfacic_nitrogen_min)<0, in these cases there is no net assimilation (Farquhar, 1980; Caemmerer, 2000) Ag, An = 0, 0 else: @@ -335,9 +335,9 @@ def calculate_photosynthesis(PAR, surfacic_nitrogen, NSC_Retroinhibition, surfac def calculate_surfacic_nitrogen(nitrates, amino_acids, proteins, Nstruct, green_area): """Surfacic content of nitrogen - :param float nitrates: amount of nitrates (µmol N) - :param float amino_acids: amount of amino_acids (µmol N) - :param float proteins: amount of proteins (µmol N) + :param float nitrates: amount of nitrates (µ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 (g) :param float green_area: green area (m-2) @@ -351,9 +351,9 @@ def calculate_surfacic_nitrogen(nitrates, amino_acids, proteins, Nstruct, green_ def calculate_surfacic_nonstructural_nitrogen(nitrates, amino_acids, proteins, green_area): """Surfacic content of non-structural nitrogen - :param float nitrates: amount of nitrates (µmol N) - :param float amino_acids: amount of amino_acids (µmol N) - :param float proteins: amount of proteins (µmol N) + :param float nitrates: amount of nitrates (µmol N) + :param float amino_acids: amount of amino_acids (µmol N) + :param float proteins: amount of proteins (µmol N) :param float green_area: green area (m-2) :return: Surfacic non-structural nitrogen (g m-2) @@ -366,7 +366,7 @@ def calculate_surfacic_nonstructural_nitrogen(nitrates, amino_acids, proteins, g def calculate_surfacic_photosynthetic_proteins(proteins, green_area): """Surfacic content of photosynthetic proteins - :param float proteins: amount of proteins (µmol N) + :param float proteins: amount of proteins (µmol N) :param float green_area: green area (m-2) :return: Surfacic non-structural nitrogen (g m-2) @@ -379,7 +379,7 @@ def calculate_surfacic_photosynthetic_proteins(proteins, green_area): def calculate_surfacic_nonstructural_nitrogen_Farquhar(surfacic_photosynthetic_proteins): """Estimate of non structural SLN used in Farquhar - :param float surfacic_photosynthetic_proteins: surfacic proteins content (µmol N m-2) + :param float surfacic_photosynthetic_proteins: surfacic proteins content (µmol N m-2) :return: Surfacic non-structural nitrogen (g m-2) :rtype: float @@ -390,12 +390,12 @@ def calculate_surfacic_nonstructural_nitrogen_Farquhar(surfacic_photosynthetic_p def calculate_surfacic_WSC(sucrose, starch, fructan, green_area): """Surfacic content of water soluble carbohydrates # TODO: rename because starch is not water-soluble. - :param float sucrose: amount of sucrose (µmol C) - :param float starch: amount of starch (µmol C) - :param float fructan: amount of fructan (µmol C) + :param float sucrose: amount of sucrose (µmol C) + :param float starch: amount of starch (µmol C) + :param float fructan: amount of fructan (µmol C) :param float green_area: green area (m-2) - :return: Surfacic content of water soluble carbohydrates (µmol C m-2) + :return: Surfacic content of water soluble carbohydrates (µmol C m-2) :rtype: float """ return (sucrose + starch + fructan) / green_area diff --git a/src/openalea/cnwgrass/gasexchange/simulation.py b/src/openalea/cnwgrass/gasexchange/simulation.py index 033336f..24cecee 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) """ @@ -126,11 +126,11 @@ 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) if element_inputs['Ci'] is None or np.isnan(element_inputs['Ci']): 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..ebc0fe7 100644 --- a/src/openalea/cnwgrass/growth/simulation.py +++ b/src/openalea/cnwgrass/growth/simulation.py @@ -262,13 +262,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 bffe38a..6fdff75 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) @@ -730,14 +730,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 @@ -745,8 +745,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 @@ -856,11 +856,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/hydraulics/parameters.py b/src/openalea/cnwgrass/hydraulics/parameters.py index 7b9f134..102828d 100644 --- a/src/openalea/cnwgrass/hydraulics/parameters.py +++ b/src/openalea/cnwgrass/hydraulics/parameters.py @@ -73,7 +73,7 @@ class AxisInitCompartments(object): """ def __init__(self): # state parameters - self.SAM_temperature = 12 #: initial temperature of shoot apical meristem (�C) + self.SAM_temperature = 12 #: initial temperature of shoot apical meristem (°C) #: The instance of class :class:`hydraulics.parameters.PhytomerInitCompartments` for current process AXIS_INIT_COMPARTMENTS = AxisInitCompartments() @@ -127,16 +127,16 @@ def __init__(self): self.phi_initial = {'x': 13E-09, 'y': 10E-09, 'z': 34E-06} #: Initial dimension-specific cell wall extensibility in relation to non-reversible dimensional changes (MPa-1 s-1) # Length - self.tend = 2160000 #: end of leaf elongation in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 - self.tmax = 1473120 #: time at which leaf elongation rate is maximal in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 - self.tbase = -822960 #: beginning of leaf elongation in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 + self.tend = 2160000 #: end of leaf elongation in automate growth (s at 12°C); fitted from adapted data from Fournier 2005 + self.tmax = 1473120 #: time at which leaf elongation rate is maximal in automate growth (s at 12°C); fitted from adapted data from Fournier 2005 + self.tbase = -822960 #: beginning of leaf elongation in automate growth (s at 12°C); fitted from adapted data from Fournier 2005 # Parameters for temperature responses - self.Temp_Tref = 12 # Arbitrary reference temperature (�C) + self.Temp_Tref = 12 # Arbitrary reference temperature (°C) self.Temp_Ea_R = 8900 # Parameter Ea/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (K) self.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) self.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) - self.Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(�C) + self.Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(°C) # Maximal leaf length self.leaf_Lmax_MAX = 0.45 #: Maximum leaf_Lmax (m) (Gauthier et al., 2021) @@ -172,9 +172,9 @@ class HiddenZoneInitCompartments(object): def __init__(self): self.Tr = 0 #: mmol H20 m-2 s-1 self.green_area = 0 #: m2 - self.temperature = 8 #: �C - self.hiddenzone_age = 0 #: �Cd - self.leaf_pseudo_age = -1 #: �Cd + self.temperature = 8 #: °C + self.hiddenzone_age = 0 #: °Cd + self.leaf_pseudo_age = -1 #: °Cd self.amino_acids = 0.000075 #: :math:`:math:`\\mu mol N`` self.proteins = 0.0011 #: :math:`:math:`\\mu mol N`` self.sucrose = 0.000384 #: :math:`:math:`\\mu mol C`` @@ -281,14 +281,14 @@ class PhotosyntheticOrganElementInitCompartments(object): def __init__(self): # state parameters self.is_growing = None #: - - self.age = None #: �Cd - self.Wmax = None #: �Cd + self.age = None #: °Cd + self.Wmax = None #: °Cd self.amino_acids = 0 #: :math:`:math:`\\mu mol N`` self.proteins = 0 #: :math:`:math:`\\mu mol N`` self.sucrose = 0 #: :math:`:math:`\\mu mol C`` self.fructan = 0 #: :math:`:math:`\\mu mol C`` - self.temperature = 0 #: �C - self.Ts = 12 #: �C + self.temperature = 0 #: °C + self.Ts = 12 #: °C self.green_area = 1E-4 #: initial value of green_area (m2) self.Tr = 0 #: mmol H20 m-2 s-1 self.mstruct = 0 #: g @@ -396,8 +396,8 @@ class SoilParameters(object): """ def __init__(self): self.AWC = 50 # Available Water Capacity (g) - self.Soil_a = 15.906 #: Mpa - Parameter for soil water function (adapt� pour sol limono-argileux profond, Grignon) - self.Soil_b = 18.902 #: % - Parameter for soil water function (adapt� pour sol limono-argileux profond, Grignon) + self.Soil_a = 15.906 #: Mpa - Parameter for soil water function (adapté pour sol limono-argileux profond, Grignon) + self.Soil_b = 18.902 #: % - Parameter for soil water function (adapté pour sol limono-argileux profond, Grignon) #: The instance of class :class:`cnmetabolism.parameters.SoilParameters` for current process SOIL_PARAMETERS = SoilParameters() diff --git a/src/openalea/cnwgrass/hydraulics/postprocessing.py b/src/openalea/cnwgrass/hydraulics/postprocessing.py index 7f380ec..a3ce14f 100644 --- a/src/openalea/cnwgrass/hydraulics/postprocessing.py +++ b/src/openalea/cnwgrass/hydraulics/postprocessing.py @@ -136,9 +136,9 @@ def calculate_LER(leaf_L, init_leaf_L, delta_t): def calculate_conc_solutes_vol(sucrose, fructan, amino_acids, volume): """Volumic concentration of solutes used for osmotic water potential calculation - :param float sucrose: Amount of sucrose (µmol` C) - :param float amino_acids: Amount of amino acids (µmol` N) - :param float fructan: Amount of fructan (µmol` C) + :param float sucrose: Amount of sucrose (µmol C) + :param float amino_acids: Amount of amino acids (µmol N) + :param float fructan: Amount of fructan (µmol C) :param float volume: Volume (m3) :return: Solutes volumic concentration (mol m-3) @@ -153,9 +153,9 @@ def calculate_conc_solutes_vol(sucrose, fructan, amino_acids, volume): def calculate_conc_solutes_mass(fructan, sucrose, amino_acids, mstruct): """Massic concentration of solutes used for osmotic water potential calculation - :param float sucrose: Amount of sucrose (µmol` C) - :param float amino_acids: Amount of amino acids (µmol` N) - :param float fructan: Amount of fructan (µmol` C) + :param float sucrose: Amount of sucrose (µmol C) + :param float amino_acids: Amount of amino acids (µmol N) + :param float fructan: Amount of fructan (µmol C) :param float mstruct: Structural mass (g) diff --git a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py index eaa6d97..99528ea 100644 --- a/src/openalea/cnwgrass/integration/cnmetabolism_facade.py +++ b/src/openalea/cnwgrass/integration/cnmetabolism_facade.py @@ -138,8 +138,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. """ @@ -225,8 +225,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/senescence_facade.py b/src/openalea/cnwgrass/integration/senescence_facade.py index fb177f9..cb0893c 100644 --- a/src/openalea/cnwgrass/integration/senescence_facade.py +++ b/src/openalea/cnwgrass/integration/senescence_facade.py @@ -217,7 +217,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 b6f436b..3831249 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 @@ -243,9 +243,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 +317,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 +479,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 +504,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 +527,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 +548,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) @@ -688,9 +688,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 +772,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 +932,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 67e7db4..d6b1b16 100644 --- a/src/openalea/cnwgrass/morphogenesis/parameters.py +++ b/src/openalea/cnwgrass/morphogenesis/parameters.py @@ -17,35 +17,35 @@ def __init__(self): # --- SAM # ------------------------------------------------------------------------------------------------------------------- # Parameters for temperature responses - self.Temp_Tref = 12 # Arbitrary reference temperature (�C) + self.Temp_Tref = 12 # Arbitrary reference temperature (°C) self.Temp_Ea_R = 8900 # Parameter Ea/R in Eyring equation from Johnson and Lewin (1946) - Parameter value fitted from Kemp and Blacklow (1982) (K) self.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) self.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) - self.Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(�C) + self.Temp_Ttransition = 9 # Below this temperature f = linear function of temperature instead of Arrhenius-like(°C) # Development - self.PLASTOCHRONE = 70 / self.Temp_Tref * 24 * 3600 #76.1/12*24*3600 #: Leaf plastochron (s at 12�C) calculated from Ljutovac 2002 with primordia of 5E-5 m (76 dd) ; Malvoisin 35dd associated with init 3E-5 m + self.PLASTOCHRONE = 70 / self.Temp_Tref * 24 * 3600 #76.1/12*24*3600 #: Leaf plastochron (s at 12°C) calculated from Ljutovac 2002 with primordia of 5E-5 m (76 dd) ; Malvoisin 35dd associated with init 3E-5 m self._delta_TT_GA = None self.max_nb_leaves = 11 #: Max number of leaves per axis # ------------------------------------------------------------------------------------------------------------------- # --- Leaves # ------------------------------------------------------------------------------------------------------------------- - self.conc_sucrose_offset = 240 #: sucrose concentration under which no leaf and internode growth occurs (�mol of C g-1 mstruct) - same for root growth, see growth + self.conc_sucrose_offset = 240 #: sucrose concentration under which no leaf and internode growth occurs (µmol of C g-1 mstruct) - same for root growth, see growth # Exponential elongation self.RERmax_Ljutovac_fit = {2: 2.55e-06, 3: 1.85e-06, 4: 1.7e-06, 5: 1.6e-06, 6: 1.5e-06, 7: 1.5e-06, 8: 1.5e-06, 9: 1.5e-06, 10: 1.4e-06, - 11: 1.4e-06} # Optimal RERmax (s-1 at 12�C) calibrated from Ljutovac (2002) - # { 5 : 0.00000279 , 6 : 0.00000176 , 7 : 0.00000162 , 8 : 0.00000144 , 9 : 0.00000144 , 10 : 0.00000144 , 11 : 0.00000142 } # Observed RER on data of Ljutovac 2002 RER (s-1 at 12�C) - #RERmax = {2: 2.1e-06, 3: 2.5e-06, 4: 2.68e-06, 5: 2.65e-06, 6: 2.65e-06, 7: 2.65e-06, 8: 2.5e-06, 9: 2.3e-06, 10: 2.2e-06, 11: 2.2e-06} # RERmax (s-1 at 12�C) fitted for simulations accounting for metabolic regulation - # self.RERmax = {2: 2.e-06, 3: 2.33e-06, 4: 2.19e-06, 5: 2.19e-06, 6: 2.25e-06, 7: 2.15e-06, 8: 2.1e-06, 9: 1.9e-06, 10: 1.75e-06, 11: 1.7e-06} # RERmax (s-1 at 12�C) fitted for simulations accounting for metabolic regulation + 11: 1.4e-06} # Optimal RERmax (s-1 at 12°C) calibrated from Ljutovac (2002) + # { 5 : 0.00000279 , 6 : 0.00000176 , 7 : 0.00000162 , 8 : 0.00000144 , 9 : 0.00000144 , 10 : 0.00000144 , 11 : 0.00000142 } # Observed RER on data of Ljutovac 2002 RER (s-1 at 12°C) + #RERmax = {2: 2.1e-06, 3: 2.5e-06, 4: 2.68e-06, 5: 2.65e-06, 6: 2.65e-06, 7: 2.65e-06, 8: 2.5e-06, 9: 2.3e-06, 10: 2.2e-06, 11: 2.2e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation + # self.RERmax = {2: 2.e-06, 3: 2.33e-06, 4: 2.19e-06, 5: 2.19e-06, 6: 2.25e-06, 7: 2.15e-06, 8: 2.1e-06, 9: 1.9e-06, 10: 1.75e-06, 11: 1.7e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation self.RERmax = {2: 1.85e-06, 3: 2.57e-06, 4: 2.33e-06, 5: 2.22e-06, 6: 2.18e-06, 7: 2.05e-06, 8: 1.9e-06, 9: 1.75e-06, 10: 1.7e-06, - 11: 1.7e-06} # RERmax (s-1 at 12�C) fitted for simulations accounting for metabolic regulation + 11: 1.7e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation - self.RER_Kc = 100 #: affinity coefficient of RER to C (�mol g-1) - self.RER_Kn = 15 #: affinity coefficient of RER to N (�mol g-1) + self.RER_Kc = 100 #: affinity coefficient of RER to C (µmol g-1) + self.RER_Kn = 15 #: affinity coefficient of RER to N (µmol g-1) # Automate elongation #- Parameters for leaf 1 and 2 @@ -53,15 +53,15 @@ def __init__(self): self.tm_init = 150 * 3600 * 24 / 12 self.tb_init = -35 * 3600 * 24 / 12 # - Following leaves - self.te = 300 * 3600 * 24 / self.Temp_Tref #: end of leaf elongation in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 - self.tm = 204.6 * 3600 * 24 / self.Temp_Tref #: time at which leaf elongation rate is maximal in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 - self.tb = -114.3 * 3600 * 24 / self.Temp_Tref #: beginning of leaf elongation in automate growth (s at 12�c); fitted from adapted data from Fournier 2005 + self.te = 300 * 3600 * 24 / self.Temp_Tref #: end of leaf elongation in automate growth (s at 12°C); fitted from adapted data from Fournier 2005 + self.tm = 204.6 * 3600 * 24 / self.Temp_Tref #: time at which leaf elongation rate is maximal in automate growth (s at 12°C); fitted from adapted data from Fournier 2005 + self.tb = -114.3 * 3600 * 24 / self.Temp_Tref #: beginning of leaf elongation in automate growth (s at 12°C); fitted from adapted data from Fournier 2005 # NB : Previous fit on adapted data from Fournier 2005 in phyllochronic time te = 271, tm=176, tb=-25 self.leaf_Lmax_MAX = 0.45 #: Maximum leaf_Lmax (m) self.leaf_pseudo_age_Vmax = 1.2 #: Maximal regulation of leaf length after emergence by CN status (dimensionless) - self.leaf_pseudo_age_Kc = 150 #: affinity coefficient to C (�mol g-1) - self.leaf_pseudo_age_Kn = 4 #: affinity coefficient to N (�mol g-1) + self.leaf_pseudo_age_Kc = 150 #: affinity coefficient to C (µmol g-1) + self.leaf_pseudo_age_Kn = 4 #: affinity coefficient to N (µmol g-1) # Leaf maximal width #- If optimal growth used @@ -84,7 +84,7 @@ def __init__(self): self.leaf_SSLW_MIN = 5. self.leaf_SSLW_MAX = 45. self.leaf_SSLW_a = 47.50516 # g m-2 - self.leaf_SSLW_b = 2927.944 # �mol C g-1 + self.leaf_SSLW_b = 2927.944 # µmol C g-1 self.leaf_LSSW_dict = {1: 0.06, 2: 0.08, 3: 0.08, 4: 0.18, 5: 0.17, 6: 0.21, 7: 0.24, 8: 0.4, 9: 0.5, 10: 0.55, 11: 0.65} # Manip NEMA 05/06 Soissons N+ (from data of J. Bertheloot, 2004) @@ -115,10 +115,10 @@ def __init__(self): # ------------------------------------------------------------------------------------------------------------------- # 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 - 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 + # 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 + 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 + # { 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. # Because i) not enough data if we consider only up to previous leaf ligulation, ii) same exponential like period @@ -153,7 +153,7 @@ def delta_TT_GA(self): if self._delta_TT_GA is not None: return self._delta_TT_GA else: - return self.PLASTOCHRONE * 5 #: Thermal time between floral transition of SAM and Gibberelin production expressed as a function of plastochron (s at 12�C) ; Malvoisin's data give 7 plastochrons + return self.PLASTOCHRONE * 5 #: Thermal time between floral transition of SAM and Gibberelin production expressed as a function of plastochron (s at 12°C) ; Malvoisin's data give 7 plastochrons @delta_TT_GA.setter def delta_TT_GA(self, value): @@ -165,7 +165,7 @@ class ParametersHydraulics(Parameters): def __init__(self): super().__init__() self.PLASTOCHRONE = 76.1 / self.Temp_Tref * 24 * 3600 - self.RERmax = {1: 2.4E-06, 2: 2.4E-06, 3: 1.85E-06, 4: 3.E-08, 5: 3.45e-06, 6: 2.3e-06, 7: 2.2e-06, 8: 2.1e-06, 9: 2.0e-06, 10: 1.85e-06, 11: 1.76e-06} # RERmax (s-1 at 12�C) fitted for simulations accounting for metabolic regulation + self.RERmax = {1: 2.4E-06, 2: 2.4E-06, 3: 1.85E-06, 4: 3.E-08, 5: 3.45e-06, 6: 2.3e-06, 7: 2.2e-06, 8: 2.1e-06, 9: 2.0e-06, 10: 1.85e-06, 11: 1.76e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation self.n = 4 #: Shape parameter of the xylem water potential function in RER (-) # self.psi_ref = -0.185 # Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) self.psi_ref = -1. #: Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) @@ -229,18 +229,18 @@ def __init__(self): self.ratio_DZ = 1.0 # Default values used for RER calculation in morphogenesis - self.sucrose = 5E-6 #: �mol C - self.amino_acids = 4E-6 #: �mol N - self.fructan = 0 #: �mol C - about 10% DM + self.sucrose = 5E-6 #: µmol C + self.amino_acids = 4E-6 #: µmol N + self.fructan = 0 #: µmol C - about 10% DM self.leaf_enclosed_mstruct = 1.26E-07 #: g self.internode_enclosed_mstruct = 0 #: g self.mstruct = self.leaf_enclosed_mstruct + self.internode_enclosed_mstruct #: g self.leaf_enclosed_Nstruct = self.leaf_enclosed_mstruct * 0.005 #: g, parameter value in Growth self.internode_enclosed_Nstruct = self.internode_enclosed_mstruct * 0.0322 #: g, parameter value in Growth self.Nstruct = self.leaf_enclosed_Nstruct + self.internode_enclosed_Nstruct #: g - self.proteins = 2.6E-03 #: �mol N - about 9% N + self.proteins = 2.6E-03 #: µmol N - about 9% N self.conc_cytokinins = 150 #: AU / g mstruct - self.mean_conc_sucrose = 0 #: �mol C / g mstruct + self.mean_conc_sucrose = 0 #: µmol C / g mstruct class HiddenZoneInitHydraulics(HiddenZoneInit): @@ -269,14 +269,14 @@ def __init__(self): self.green_area = 0 #: m2 self.age = 0 #: Thermal Time self.age_teq = 0 #: s - self.max_proteins = 0 #: �mol N + self.max_proteins = 0 #: µmol N self.Nresidual = 0 #: g - self.sucrose = 0 #: �mol C - self.amino_acids = 0 #: �mol N - self.nitrates = 0 #: �mol N - self.fructan = 0 #: �mol C - self.starch = 0 #: �mol C - self.proteins = 0 #: �mol N + self.sucrose = 0 #: µmol C + self.amino_acids = 0 #: µmol N + self.nitrates = 0 #: µmol N + self.fructan = 0 #: µmol C + self.starch = 0 #: µmol C + self.proteins = 0 #: µmol N self.mstruct = 0 #: g self.senesced_mstruct = 0 #: g self.max_mstruct = 0 #: g diff --git a/src/openalea/cnwgrass/morphogenesis/simulation.py b/src/openalea/cnwgrass/morphogenesis/simulation.py index ead0529..12276aa 100644 --- a/src/openalea/cnwgrass/morphogenesis/simulation.py +++ b/src/openalea/cnwgrass/morphogenesis/simulation.py @@ -165,7 +165,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 From 746817075020bcc1467bb7e0dfd0264c44684dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Thu, 24 Sep 2026 21:00:34 +0200 Subject: [PATCH 18/21] last_commit --- src/openalea/cnwgrass/hydraulics/parameters.py | 2 +- src/openalea/cnwgrass/hydraulics/simulation.py | 10 +++++----- src/openalea/cnwgrass/morphogenesis/parameters.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/parameters.py b/src/openalea/cnwgrass/hydraulics/parameters.py index 102828d..a5fbe77 100644 --- a/src/openalea/cnwgrass/hydraulics/parameters.py +++ b/src/openalea/cnwgrass/hydraulics/parameters.py @@ -151,7 +151,7 @@ def __init__(self): self.SLOPE_MASS_VOLUME = 3.23337E-06 #: Slope of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3 g-1). Found from Williams 1960, Fig 11. self.OFFSET_MASS_VOLUME = 1.82312E-13 #: Offset of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3). Found from Williams 1960, Fig 11. # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. - self.GAMMA = 0.11 + self.GAMMA = 0.15 self.Sa = 260 #: (mol m-3) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential self.Sb = 0.9 #: (-) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential diff --git a/src/openalea/cnwgrass/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index 479c8f7..853ca6b 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -580,11 +580,11 @@ def run(self): 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) - # alpha_relaxation_flux = 1. - # for plant in self.population.plants: - # for axis in plant.axes: - # axis.xylem.root_to_shoot_xylem_water_flow = alpha_relaxation_flux * axis.xylem.root_to_shoot_xylem_water_flow + (1. - alpha_relaxation_flux) * self.old_root_to_shoot_xylem_water_flow - # self.old_root_to_shoot_xylem_water_flow = axis.xylem.root_to_shoot_xylem_water_flow + alpha_relaxation_flux = 1. + for plant in self.population.plants: + for axis in plant.axes: + axis.xylem.root_to_shoot_xylem_water_flow = alpha_relaxation_flux * axis.xylem.root_to_shoot_xylem_water_flow + (1. - alpha_relaxation_flux) * self.old_root_to_shoot_xylem_water_flow + self.old_root_to_shoot_xylem_water_flow = axis.xylem.root_to_shoot_xylem_water_flow self.nfev_total += sol.nfev diff --git a/src/openalea/cnwgrass/morphogenesis/parameters.py b/src/openalea/cnwgrass/morphogenesis/parameters.py index d6b1b16..adacb2c 100644 --- a/src/openalea/cnwgrass/morphogenesis/parameters.py +++ b/src/openalea/cnwgrass/morphogenesis/parameters.py @@ -173,7 +173,7 @@ def __init__(self): 8: 0.19288286, 9: 0.25080917, 10: 0.28677033, 11: 0.3} #: Maximum lamina length (m) # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. - self.GAMMA = 0.11 + self.GAMMA = 0.15 def parameters_factory(hydraulics=False): From df13bf52cb925397f53064f8ed393400737cdebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Sat, 26 Sep 2026 20:57:03 +0200 Subject: [PATCH 19/21] gamma --- src/openalea/cnwgrass/hydraulics/parameters.py | 2 +- src/openalea/cnwgrass/morphogenesis/parameters.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/parameters.py b/src/openalea/cnwgrass/hydraulics/parameters.py index a5fbe77..3efa491 100644 --- a/src/openalea/cnwgrass/hydraulics/parameters.py +++ b/src/openalea/cnwgrass/hydraulics/parameters.py @@ -151,7 +151,7 @@ def __init__(self): self.SLOPE_MASS_VOLUME = 3.23337E-06 #: Slope of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3 g-1). Found from Williams 1960, Fig 11. self.OFFSET_MASS_VOLUME = 1.82312E-13 #: Offset of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3). Found from Williams 1960, Fig 11. # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. - self.GAMMA = 0.15 + self.GAMMA = 0.16 self.Sa = 260 #: (mol m-3) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential self.Sb = 0.9 #: (-) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential diff --git a/src/openalea/cnwgrass/morphogenesis/parameters.py b/src/openalea/cnwgrass/morphogenesis/parameters.py index adacb2c..96503d2 100644 --- a/src/openalea/cnwgrass/morphogenesis/parameters.py +++ b/src/openalea/cnwgrass/morphogenesis/parameters.py @@ -167,13 +167,13 @@ def __init__(self): self.PLASTOCHRONE = 76.1 / self.Temp_Tref * 24 * 3600 self.RERmax = {1: 2.4E-06, 2: 2.4E-06, 3: 1.85E-06, 4: 3.E-08, 5: 3.45e-06, 6: 2.3e-06, 7: 2.2e-06, 8: 2.1e-06, 9: 2.0e-06, 10: 1.85e-06, 11: 1.76e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation self.n = 4 #: Shape parameter of the xylem water potential function in RER (-) - # self.psi_ref = -0.185 # Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) - self.psi_ref = -1. #: Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) + self.psi_ref = -0.185 # Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) + # self.psi_ref = -1. #: Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) self.lamina_Lmax_dict = {3: 0.09896253, 4: 0.10463127, 5: 0.10867923, 6: 0.1352669, 7: 0.16634665, 8: 0.19288286, 9: 0.25080917, 10: 0.28677033, 11: 0.3} #: Maximum lamina length (m) # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. - self.GAMMA = 0.15 + self.GAMMA = 0.16 def parameters_factory(hydraulics=False): From 46a5a77e2d290e43072f29f3ec43f4750f8d936c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Sun, 4 Oct 2026 22:35:44 +0200 Subject: [PATCH 20/21] sync_ceci --- src/openalea/cnwgrass/hydraulics/parameters.py | 2 +- src/openalea/cnwgrass/morphogenesis/parameters.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/parameters.py b/src/openalea/cnwgrass/hydraulics/parameters.py index 3efa491..77f7ad9 100644 --- a/src/openalea/cnwgrass/hydraulics/parameters.py +++ b/src/openalea/cnwgrass/hydraulics/parameters.py @@ -151,7 +151,7 @@ def __init__(self): self.SLOPE_MASS_VOLUME = 3.23337E-06 #: Slope of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3 g-1). Found from Williams 1960, Fig 11. self.OFFSET_MASS_VOLUME = 1.82312E-13 #: Offset of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3). Found from Williams 1960, Fig 11. # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. - self.GAMMA = 0.16 + self.GAMMA = 0.13 self.Sa = 260 #: (mol m-3) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential self.Sb = 0.9 #: (-) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential diff --git a/src/openalea/cnwgrass/morphogenesis/parameters.py b/src/openalea/cnwgrass/morphogenesis/parameters.py index 96503d2..bafe14b 100644 --- a/src/openalea/cnwgrass/morphogenesis/parameters.py +++ b/src/openalea/cnwgrass/morphogenesis/parameters.py @@ -167,13 +167,13 @@ def __init__(self): self.PLASTOCHRONE = 76.1 / self.Temp_Tref * 24 * 3600 self.RERmax = {1: 2.4E-06, 2: 2.4E-06, 3: 1.85E-06, 4: 3.E-08, 5: 3.45e-06, 6: 2.3e-06, 7: 2.2e-06, 8: 2.1e-06, 9: 2.0e-06, 10: 1.85e-06, 11: 1.76e-06} # RERmax (s-1 at 12°C) fitted for simulations accounting for metabolic regulation self.n = 4 #: Shape parameter of the xylem water potential function in RER (-) - self.psi_ref = -0.185 # Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) - # self.psi_ref = -1. #: Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) + # self.psi_ref = -0.185 # Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) + self.psi_ref = -1. #: Critical leaf water potential threshold at which RER is reduced by 50% (Mpa) (adapted from Boyer, 1968) self.lamina_Lmax_dict = {3: 0.09896253, 4: 0.10463127, 5: 0.10867923, 6: 0.1352669, 7: 0.16634665, 8: 0.19288286, 9: 0.25080917, 10: 0.28677033, 11: 0.3} #: Maximum lamina length (m) # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. - self.GAMMA = 0.16 + self.GAMMA = 0.13 def parameters_factory(hydraulics=False): From d18d4c6c943d7bc783182403b7d5fae55acd4a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20G=C3=A9rault?= Date: Fri, 16 Oct 2026 20:18:21 +0200 Subject: [PATCH 21/21] Stale emergence and internode elongation fixes #41 #42 --- .../cnwgrass/hydraulics/parameters.py | 2 +- .../cnwgrass/hydraulics/simulation.py | 66 +++++++++++++++---- src/openalea/cnwgrass/morphogenesis/model.py | 23 ++++++- .../cnwgrass/morphogenesis/parameters.py | 15 ++++- .../cnwgrass/morphogenesis/simulation.py | 47 ++++++++++++- 5 files changed, 134 insertions(+), 19 deletions(-) diff --git a/src/openalea/cnwgrass/hydraulics/parameters.py b/src/openalea/cnwgrass/hydraulics/parameters.py index 77f7ad9..102828d 100644 --- a/src/openalea/cnwgrass/hydraulics/parameters.py +++ b/src/openalea/cnwgrass/hydraulics/parameters.py @@ -151,7 +151,7 @@ def __init__(self): self.SLOPE_MASS_VOLUME = 3.23337E-06 #: Slope of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3 g-1). Found from Williams 1960, Fig 11. self.OFFSET_MASS_VOLUME = 1.82312E-13 #: Offset of the relation between leaf dry mass and its volume at the time of the previous leaf emergence (m3). Found from Williams 1960, Fig 11. # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. - self.GAMMA = 0.13 + self.GAMMA = 0.11 self.Sa = 260 #: (mol m-3) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential self.Sb = 0.9 #: (-) Parameter of a sigmoidal function of equivalent solutes concentration used in osmotic water potential diff --git a/src/openalea/cnwgrass/hydraulics/simulation.py b/src/openalea/cnwgrass/hydraulics/simulation.py index 853ca6b..e1c8230 100644 --- a/src/openalea/cnwgrass/hydraulics/simulation.py +++ b/src/openalea/cnwgrass/hydraulics/simulation.py @@ -816,7 +816,7 @@ def _calculate_all_derivatives(self, t, y): # Update of leaf_Lmax hiddenzone.leaf_Lmax = hiddenzone.leaf_L - if hiddenzone.leaf_pseudo_age == 0: #: First time after previous leaf emergence + if hiddenzone.leaf_pseudo_age == 0: #: Before the start of turgor-driven growth (dimensions from morphogenesis) #: Width and thickness thickness_ratio = parameters.HIDDEN_ZONE_PARAMETERS.TL_ratio width_ratio = parameters.HIDDEN_ZONE_PARAMETERS.WL_ratio @@ -824,18 +824,24 @@ def _calculate_all_derivatives(self, t, y): hiddenzone.width = hiddenzone.leaf_L * width_ratio hiddenzone.thickness = hiddenzone.leaf_L * thickness_ratio hiddenzone.leaf_Wmax = hiddenzone.width + #: Length (NOTE: now computed before the volume, which used the length of the previous step) + hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) - #: Volume & water content as function of dimensions - hiddenzone.volume = hiddenzone.length * hiddenzone.width * hiddenzone.thickness - hiddenzone.water_content = hiddenzone.volume * parameters.RHO_WATER + # NOTE (stalled emergence fix): the water content used to be recomputed from the dimensions at every call + # (volume = length * width * thickness, i.e. proportional to leaf_L^3), which created or destroyed water without + # any flux. While leaf_pseudo_age stays at 0 (turgor <= GAMMA), this reached grams of water (28.8 g at the + # emergence of the previous leaf in a GAMMA = 0.17 run), diluted the solutes so that turgor could never exceed + # GAMMA again, and destabilized the shoot water balance. The water content is now a state variable, and the + # volume given by the dimensions is only a target filled by a water influx from the xylem (see below). + hiddenzone.target_water_content = hiddenzone.length * hiddenzone.width * hiddenzone.thickness * parameters.RHO_WATER + hiddenzone.water_content = max(y[self.initial_conditions_mapping[hiddenzone]['water_content']], 0.) + hiddenzone.volume = hiddenzone.calculate_volume(max(hiddenzone.water_content, 1e-12)) #: Osmotic water potential hiddenzone.osmotic_water_potential = hiddenzone.calculate_osmotic_water_potential(hiddenzone.fructan, hiddenzone.sucrose, hiddenzone.amino_acids, hiddenzone.volume, axis.SAM_temperature) - #: Total water potential + #: Total water potential (in equilibrium with the xylem, as before) hiddenzone.water_potential = axis.xylem.water_potential #: Turgor water potential hiddenzone.turgor_water_potential = hiddenzone.water_potential - hiddenzone.osmotic_water_potential - #: Length - hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) elif hiddenzone.leaf_pseudo_age > 0: #: After previous leaf emergence #: Turgor water potential hiddenzone.turgor_water_potential = y[self.initial_conditions_mapping[hiddenzone]['turgor_water_potential']] @@ -856,6 +862,13 @@ def _calculate_all_derivatives(self, t, y): hiddenzone.resistance = hiddenzone.calculate_resistance(hiddenzone_dimensions) #: Flows with xylem hiddenzone.water_influx = hiddenzone.calculate_water_flux(hiddenzone.water_potential, axis.xylem.water_potential, hiddenzone.resistance, self.delta_t) + if hiddenzone.leaf_pseudo_age == 0: + # NOTE (stalled emergence fix): water needed to reach the volume set by the dimensions is drawn from the xylem. + # Its rate is at most the flux allowed by the hydraulic resistance under the full osmotic gradient (zero turgor), + # and it relaxes the deficit within one time step. Water is never created and the zone never shrinks here. + max_influx = max(hiddenzone.calculate_water_flux(hiddenzone.osmotic_water_potential, axis.xylem.water_potential, hiddenzone.resistance, self.delta_t), 0.) + water_deficit = max(hiddenzone.target_water_content - hiddenzone.water_content, 0.) + hiddenzone.water_influx = min(max_influx, water_deficit / self.time_step) hiddenzone.water_outflow = 0 #: No water flow between hiddenzone and element @@ -970,6 +983,12 @@ def _calculate_all_derivatives(self, t, y): hiddenzone.organ_volume = hiddenzone.calculate_organ_volume(hiddenzone_dimensions) hiddenzone.WC_mstruct = hiddenzone.water_content / (hiddenzone.water_content + hiddenzone.mstruct) * 100 + elif hiddenzone.leaf_pseudo_age == 0: #: Before the start of turgor-driven growth + # NOTE (stalled emergence fix): only the water content evolves, through the influx computed above. + # Dimensions are driven by morphogenesis (no plastic or elastic deformation in this phase, as before). + y_derivatives[self.initial_conditions_mapping[hiddenzone]['water_content']] = delta_water_content_hz + continue + else: #: Before previous leaf emergence continue @@ -1120,7 +1139,7 @@ def _calculate_shoot_derivatives(self, t, y): # Update of leaf_Lmax hiddenzone.leaf_Lmax = hiddenzone.leaf_L - if hiddenzone.leaf_pseudo_age == 0: #: First time after previous leaf emergence + if hiddenzone.leaf_pseudo_age == 0: #: Before the start of turgor-driven growth (dimensions from morphogenesis) #: Width and thickness thickness_ratio = parameters.HIDDEN_ZONE_PARAMETERS.TL_ratio width_ratio = parameters.HIDDEN_ZONE_PARAMETERS.WL_ratio @@ -1128,18 +1147,24 @@ def _calculate_shoot_derivatives(self, t, y): hiddenzone.width = hiddenzone.leaf_L * width_ratio hiddenzone.thickness = hiddenzone.leaf_L * thickness_ratio hiddenzone.leaf_Wmax = hiddenzone.width + #: Length (NOTE: now computed before the volume, which used the length of the previous step) + hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) - #: Volume & water content as function of dimensions - hiddenzone.volume = hiddenzone.length * hiddenzone.width * hiddenzone.thickness - hiddenzone.water_content = hiddenzone.volume * parameters.RHO_WATER + # NOTE (stalled emergence fix): the water content used to be recomputed from the dimensions at every call + # (volume = length * width * thickness, i.e. proportional to leaf_L^3), which created or destroyed water without + # any flux. While leaf_pseudo_age stays at 0 (turgor <= GAMMA), this reached grams of water (28.8 g at the + # emergence of the previous leaf in a GAMMA = 0.17 run), diluted the solutes so that turgor could never exceed + # GAMMA again, and destabilized the shoot water balance. The water content is now a state variable, and the + # volume given by the dimensions is only a target filled by a water influx from the xylem (see below). + hiddenzone.target_water_content = hiddenzone.length * hiddenzone.width * hiddenzone.thickness * parameters.RHO_WATER + hiddenzone.water_content = max(y[self.initial_conditions_mapping[hiddenzone]['water_content']], 0.) + hiddenzone.volume = hiddenzone.calculate_volume(max(hiddenzone.water_content, 1e-12)) #: Osmotic water potential hiddenzone.osmotic_water_potential = hiddenzone.calculate_osmotic_water_potential(hiddenzone.fructan, hiddenzone.sucrose, hiddenzone.amino_acids, hiddenzone.volume, axis.SAM_temperature) - #: Total water potential + #: Total water potential (in equilibrium with the xylem, as before) hiddenzone.water_potential = axis.xylem.water_potential #: Turgor water potential hiddenzone.turgor_water_potential = hiddenzone.water_potential - hiddenzone.osmotic_water_potential - #: Length - hiddenzone.length = hiddenzone.calculate_hiddenzone_length(hiddenzone.leaf_L, hiddenzone.leaf_pseudostem_length) elif hiddenzone.leaf_pseudo_age > 0: #: After previous leaf emergence #: Turgor water potential hiddenzone.turgor_water_potential = y[self.initial_conditions_mapping[hiddenzone]['turgor_water_potential']] @@ -1160,6 +1185,13 @@ def _calculate_shoot_derivatives(self, t, y): hiddenzone.resistance = hiddenzone.calculate_resistance(hiddenzone_dimensions) #: Flows with xylem hiddenzone.water_influx = hiddenzone.calculate_water_flux(hiddenzone.water_potential, axis.xylem.water_potential, hiddenzone.resistance, self.delta_t) + if hiddenzone.leaf_pseudo_age == 0: + # NOTE (stalled emergence fix): water needed to reach the volume set by the dimensions is drawn from the xylem. + # Its rate is at most the flux allowed by the hydraulic resistance under the full osmotic gradient (zero turgor), + # and it relaxes the deficit within one time step. Water is never created and the zone never shrinks here. + max_influx = max(hiddenzone.calculate_water_flux(hiddenzone.osmotic_water_potential, axis.xylem.water_potential, hiddenzone.resistance, self.delta_t), 0.) + water_deficit = max(hiddenzone.target_water_content - hiddenzone.water_content, 0.) + hiddenzone.water_influx = min(max_influx, water_deficit / self.time_step) hiddenzone.water_outflow = 0 #: No water flow between hiddenzone and element # Positionned here to capture the last computation of the last loop @@ -1279,6 +1311,12 @@ def _calculate_shoot_derivatives(self, t, y): hiddenzone.organ_volume = hiddenzone.calculate_organ_volume(hiddenzone_dimensions) hiddenzone.WC_mstruct = hiddenzone.water_content / (hiddenzone.water_content + hiddenzone.mstruct) * 100 + elif hiddenzone.leaf_pseudo_age == 0: #: Before the start of turgor-driven growth + # NOTE (stalled emergence fix): only the water content evolves, through the influx computed above. + # Dimensions are driven by morphogenesis (no plastic or elastic deformation in this phase, as before). + y_derivatives[self.initial_conditions_mapping[hiddenzone]['water_content']] = delta_water_content_hz + continue + else: #: Before previous leaf emergence continue diff --git a/src/openalea/cnwgrass/morphogenesis/model.py b/src/openalea/cnwgrass/morphogenesis/model.py index 3831249..ad6f90c 100644 --- a/src/openalea/cnwgrass/morphogenesis/model.py +++ b/src/openalea/cnwgrass/morphogenesis/model.py @@ -929,9 +929,12 @@ def __init__(self): def calculate_deltaL_preE(self, sucrose, leaf_L, amino_acids, mstruct, delta_teq, leaf_rank, optimal_growth_option, - xylem_water_potential=0.): + xylem_water_potential=0., turgor_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. + NOTE (stalled emergence fix): elongation is also limited by the hidden zone turgor, as after the emergence of the previous + leaf (see preE_turgor_growth_factor), so that a stalled leaf also stalls the leaves it encloses. + :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) @@ -959,11 +962,29 @@ def calculate_deltaL_preE(self, sucrose, leaf_L, amino_acids, mstruct, delta_teq xylem_water_potential / self.parameters.psi_ref) ** self.parameters.n) # Sigmoidal function of xylem water potential control of RER delta_leaf_L = delta_teq * leaf_L * RER_max / ((1 + self.parameters.RER_Kc / conc_sucrose_effective) * ( 1 + self.parameters.RER_Kn / conc_amino_acids)) * regul_water + delta_leaf_L *= self.preE_turgor_growth_factor(turgor_water_potential) else: delta_leaf_L = 0 return delta_leaf_L + def preE_turgor_growth_factor(self, turgor_water_potential): + """ Lockhart-type limitation of pre-emergence elongation by the hidden zone turgor (-). + 0 at or below the yield threshold GAMMA (no irreversible expansion), increasing linearly to 1 at GAMMA + preE_turgor_ramp. + + Emergence is not blocked by this factor: when elongation stops, sucrose and amino acids keep being unloaded in the + hidden zone while their consumption by growth stops, so its osmotic potential decreases and its turgor rises again above + GAMMA (this negative feedback requires the hidden zone volume to be conserved, see hydraulics simulation). + + :param float turgor_water_potential: turgor water potential of the hidden zone (MPa), None if not provided by hydraulics + + :return: growth factor (-) + :rtype: float + """ + if turgor_water_potential is None: + return 1. + return min(1., max(0., (turgor_water_potential - self.parameters.GAMMA) / self.parameters.preE_turgor_ramp)) + def calculate_leaf_pseudo_age(self, leaf_pseudo_age, delta_teq, turgor_water_potential=0): """ Pseudo age of the leaf since beginning of automate elongation (s) diff --git a/src/openalea/cnwgrass/morphogenesis/parameters.py b/src/openalea/cnwgrass/morphogenesis/parameters.py index bafe14b..a03ec29 100644 --- a/src/openalea/cnwgrass/morphogenesis/parameters.py +++ b/src/openalea/cnwgrass/morphogenesis/parameters.py @@ -173,7 +173,20 @@ def __init__(self): 8: 0.19288286, 9: 0.25080917, 10: 0.28677033, 11: 0.3} #: Maximum lamina length (m) # self.GAMMA = 0.2 #: Critical value for the pressure component which must be exceeded for irreversible volume changes (MPa). Found from Coussement et al., 2018 : 0.3 Mpa for soybean. - self.GAMMA = 0.13 + self.GAMMA = 0.11 + + # NOTE (stalled emergence fix): before the emergence of the previous leaf, leaf elongation used to ignore turgor, while + # the progression of leaf_pseudo_age (and thus the emergence of the previous leaf) requires turgor > GAMMA. With high GAMMA + # values, younger hidden leaves kept elongating exponentially behind a stalled leaf (up to 5.8 times the length of the + # leaf enclosing them), until a burst of emergences destabilized the shoot. + #: Turgor excess above GAMMA (MPa) at which pre-emergence elongation is no longer limited by turgor (Lockhart-type + #: linear ramp from 0 at turgor = GAMMA). Small value so that unstressed growth (turgor 0.1 to 0.5 MPa above GAMMA + #: in previous simulations) is unchanged. + self.preE_turgor_ramp = 0.05 + #: Maximal length of a hidden leaf relative to the length of the previous (enclosing) leaf before the latter emerges (-). + #: A leaf grows inside the sheath tube of the previous one, so its tip cannot pass the tip of the enclosing leaf. + #: Never reached in unstressed simulations (max observed ratio 0.74). + self.max_preE_leaf_L_ratio = 1.0 def parameters_factory(hydraulics=False): diff --git a/src/openalea/cnwgrass/morphogenesis/simulation.py b/src/openalea/cnwgrass/morphogenesis/simulation.py index 12276aa..5ac4753 100644 --- a/src/openalea/cnwgrass/morphogenesis/simulation.py +++ b/src/openalea/cnwgrass/morphogenesis/simulation.py @@ -96,6 +96,28 @@ def initialize(self, inputs): self.inputs.clear() self.inputs.update(inputs) + def _internode_turgor_factor(self, hiddenzone_id, all_hiddenzone_inputs): + """ + Turgor limitation of internode elongation (-), see calculate_deltaL_preE / preE_turgor_growth_factor. + + The turgor of a hidden zone is only computed by the hydraulics model while its leaf grows (leaf_is_growing and + leaf_pseudo_age >= 0); afterwards the stored value is frozen. When it is not available, the turgor of the closest hidden zone + above on the same axis whose leaf is still growing is used, as the stem growing zones share the same xylem supply. + Without any available turgor (or without hydraulics), elongation is not limited (factor 1, previous behaviour). + """ + if not self.hydraulics: + return 1. + axis_id, phytomer_id = hiddenzone_id[:2], hiddenzone_id[2] + rank = phytomer_id + while True: + hz_id = tuple(list(axis_id) + [rank]) + if hz_id not in all_hiddenzone_inputs: + return 1. + hz = all_hiddenzone_inputs[hz_id] + if hz.get('leaf_is_growing') and hz.get('leaf_pseudo_age', -1) >= 0 and hz.get('turgor_water_potential') is not None: + return self.model.preE_turgor_growth_factor(hz['turgor_water_potential']) + rank += 1 + def run(self, Tair, Tsoil, Zsowing=0.025): """ Run the simulation. @@ -349,9 +371,18 @@ def run(self, Tair, Tsoil, Zsowing=0.025): hiddenzone_inputs['mstruct'], curr_axis_outputs['delta_teq'], phytomer_id, self.optimal_growth_option, - xylem_water_potential) + xylem_water_potential, + hz_turgor_water_potential if self.hydraulics else None) leaf_L = hiddenzone_inputs['leaf_L'] + delta_leaf_L + + # NOTE (stalled emergence fix): a hidden leaf grows inside the sheath tube of the previous leaf and cannot become + # longer than it before the latter emerges. Safety bound that only acts if the previous leaf stalls (the leaf is + # never shortened). It does not delay the emergence of the previous leaf, which only depends on its own length. + if self.hydraulics and prev_hiddenzone_inputs is not None: + max_leaf_L = self.model.parameters.max_preE_leaf_L_ratio * prev_hiddenzone_inputs['leaf_L'] + leaf_L = max(hiddenzone_inputs['leaf_L'], min(leaf_L, max_leaf_L)) + delta_leaf_L = leaf_L - hiddenzone_inputs['leaf_L'] curr_hiddenzone_outputs['ratio_DZ'] = 1 else: #: After the emergence of the previous leaf. @@ -634,11 +665,20 @@ def run(self, Tair, Tsoil, Zsowing=0.025): else: prev_leaf_ligulated = False + # NOTE (water stress fix): internode elongation used to ignore water status (and is not represented in the hydraulics + # model), so under drought the stem kept elongating while turgor-limited leaves stopped. Hidden zones were pushed up + # through the sheath tube of stalled leaves until the pseudostem length reached 0, which made 1 to 4 mm leaves "emerge" + # and gave a zero hidden zone length, a zero hydraulic resistance and a NaN in the hydraulics solver. The internode + # growing zone lies in the same hidden zone as the leaf growing zone and is limited by the same turgor (same + # Lockhart-type factor as pre-emergence leaf elongation, 1 when turgor is unknown or without hydraulics). + internode_turgor_factor = self._internode_turgor_factor(hiddenzone_id, all_hiddenzone_inputs) + #: Before ligulation of the leaf on the previous phytomer. Exponential-like elong. cf. Kirby 1988, Malvoisin 1984 II if not prev_leaf_ligulated: delta_internode_L = self.model.calculate_delta_internode_L_preL(phytomer_id, curr_hiddenzone_outputs['sucrose'], curr_hiddenzone_outputs['internode_L'], curr_hiddenzone_outputs['amino_acids'], curr_hiddenzone_outputs['mstruct'], curr_axis_outputs['delta_teq']) + delta_internode_L *= internode_turgor_factor internode_L = curr_hiddenzone_outputs['internode_L'] + delta_internode_L # TODO: Check internode_L is not too large (in the case of long delta_t) curr_hiddenzone_outputs['internode_L'] = internode_L @@ -652,7 +692,10 @@ def run(self, Tair, Tsoil, Zsowing=0.025): #: After ligulation of the leaf on the previous phytomer. else: prev_internode_pseudo_age = curr_hiddenzone_outputs['internode_pseudo_age'] - internode_pseudo_age = self.model.calculate_internode_pseudo_age(prev_internode_pseudo_age, curr_axis_outputs['delta_teq']) + # NOTE (water stress fix): the internode pseudo age (which drives the Beta-function elongation and the end of elongation) + # only progresses when turgor allows irreversible expansion, as leaf_pseudo_age does. Elongation is delayed, while the + # final internode length (internode_Lmax_lig) is unchanged. + internode_pseudo_age = self.model.calculate_internode_pseudo_age(prev_internode_pseudo_age, curr_axis_outputs['delta_teq'] * internode_turgor_factor) curr_hiddenzone_outputs['internode_pseudo_age'] = internode_pseudo_age curr_hiddenzone_outputs['delta_internode_pseudo_age'] = internode_pseudo_age - curr_hiddenzone_outputs['internode_pseudo_age']