Source code for openalea.cnwgrass.gasexchange.simulation

# -*- coding: latin-1 -*-

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

import warnings
import inspect

import numpy as np

from openalea.cnwgrass.gasexchange import model
from openalea.cnwgrass.gasexchange import parameters

"""
    gasexchange.simulation
    ~~~~~~~~~~~~~~~~~~~~~~~~

    The module :mod:`gasexchange.simulation` is the front-end to run the Gas-Exchange :mod:`model <gasexchange.model>`.

"""


[docs] class SimulationError(Exception): pass
[docs] class SimulationInputsError(SimulationError): pass
[docs] class Simulation: """The Simulation class permits to initialize and run a simulation. """ def __init__(self, update_parameters=None, stomatal_model_name='BWB', hydraulics=False): """ :param None or dict update_parameters: if a dict is provided, the specified parameters in keys will be updated. :param str stomatal_model_name: the model of stomatal conductance. Should be one of 'BWB', 'Leuning', 'Tuzet' or 'hydraulics'. :param bool hydraulics: if True the model will assume the coupling to the turgor-driven growth model. """ #: `inputs` is a dictionary of dictionaries: #: {(plant_index, axis_label, metamer_index, organ_label, element_label): {element_input_name: element_input_value, ...}, ...} self.inputs = {} #: the inputs needed by Gas-Exchange at element scale self.elements_inputs = ['width', 'height', 'PARa', 'nitrates', 'amino_acids', 'proteins', 'Nstruct', 'green_area', 'sucrose', 'starch', 'fructan', 'PARa_prim', 'area_prim', 'Ci'] if hydraulics: self.elements_inputs.append('water_potential') #: the inputs needed by Gas-Exchange at axis scale self.axes_inputs = ['SAM_temperature', 'height_canopy'] #: `outputs` is a dictionary of dictionaries: #: {(plant_index, axis_label, metamer_index, organ_label, element_label): {element_output_name: element_output_value, ...}, ...} self.outputs = {} #: the outputs computed by Gas-Exchange self.elements_outputs = ['Ag', 'An', 'Rd', 'Tr', 'Ts', 'gs', 'Ci', 'width', 'height'] self.elements_inputs_outputs = set(self.elements_inputs + self.elements_outputs) #: Update parameters if specified if update_parameters: for key, value in update_parameters.items(): if hasattr(parameters, key): setattr(parameters, key, value) else: warnings.warn(f"Parameter '{key}' is not defined in class self.model.parameters.") #: Stomatal conductance model if stomatal_model_name not in model.STOMATAL_MODELS_MAPPING: raise ValueError( f"Unknown stomatal conductance model." f"Must be one of : {list(model.STOMATAL_MODELS_MAPPING.keys())}" ) HYDRAULIC_DEPENDENT_MODELS = ['Tuzet', 'hydraulics'] if stomatal_model_name in HYDRAULIC_DEPENDENT_MODELS and not hydraulics: raise ValueError( f"Configuration error: the stomatal conductance model '{stomatal_model_name}' " f"requires the hydraulics option to be True in order to calculate the water potentials." ) # Gets the selected model self.stomatal_model = model.STOMATAL_MODELS_MAPPING[stomatal_model_name] self.gs_args_keys = inspect.signature(self.stomatal_model).parameters self.hydraulics = hydraulics
[docs] def initialize(self, inputs): """ Initialize :attr:`inputs` from `inputs`. :param dict inputs: Dictionary of two dictionaries : - `elements` : The inputs by element. - `axes` : The inputs by axis. `inputs` must be a dictionary with the same structure as :attr:`inputs`. See :meth:`Model.run <gasexchange.model.run>` for more information about the inputs. """ self.inputs.clear() self.inputs.update(inputs)
[docs] def run(self, Ta, ambient_CO2, RH, Ur): """ Compute Farquhar variables for each element in :attr:`inputs` and put 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 RH: relative humidity at t (decimal fraction) :param float Ur: wind speed at the top of the canopy at t (m s-1) """ self.outputs.update({inputs_type: {} for inputs_type in self.inputs['elements'].keys()}) for (element_id, element_inputs) in self.inputs['elements'].items(): axis_id = element_id[:2] organ_label = element_id[3] axe_label = axis_id[1] if axe_label != 'MS': # Calculation only for the main stem continue # In case it is an HiddenElement, we need temperature calculation. # Cases of Visible Element without geometry property (because too small) don't have photosynthesis calculation neither. if element_inputs['height'] is None or np.isnan(element_inputs['height']): Ag, An, Rd, Tr, gsw, Ci = 0., 0., 0., 0., 0., 0. Ts = self.inputs['axes'][axis_id]['SAM_temperature'] else: Ts = Ta # Initial value of Ts (°C) if element_inputs['Ci'] is None or np.isnan(element_inputs['Ci']): 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 height_canopy = self.inputs['axes'][axis_id]['height_canopy'] water_potential = element_inputs.get('water_potential', None) if parameters.SurfacicProteins: surfacic_photosynthetic_proteins = model.calculate_surfacic_photosynthetic_proteins(element_inputs['proteins'], element_inputs['green_area']) surfacic_nitrogen = model.calculate_surfacic_nonstructural_nitrogen_Farquhar(surfacic_photosynthetic_proteins) else: surfacic_nitrogen = model.calculate_surfacic_nitrogen(element_inputs['nitrates'], element_inputs['amino_acids'], element_inputs['proteins'], element_inputs['Nstruct'], element_inputs['green_area']) if surfacic_nitrogen == 0: element_outputs = {'Ag': 0, 'An': 0, 'Rd': 0, 'Tr': 0, 'Ts': self.inputs['axes'][axis_id]['SAM_temperature'], 'gs': 0, 'Ci': 0, 'width': element_inputs['width'], 'height': element_inputs['height']} self.outputs[element_id] = element_outputs continue surfacic_NSC = model.calculate_surfacic_WSC(element_inputs['sucrose'], element_inputs['starch'], element_inputs['fructan'], element_inputs['green_area']) # Checks if the calculation are made at the whole element scale or at the primitive scale if parameters.prim_scale: PARa_list = element_inputs['PARa_prim'] areas_list = element_inputs['area_prim'] else: PARa_list = [element_inputs['PARa']] # Unique item in this case areas_list = [1.0] # Set to 1 for mean PARa calculation below # Run the model Ag_prim_list = [] for PARa in PARa_list: count = 0 # Iteration until convergence while True: prec_Ci, prec_Ts = Ci, Ts # Farquhar model calculations Ag, An, Rd = model.calculate_photosynthesis(PARa, surfacic_nitrogen, parameters.NSC_Retroinhibition, surfacic_NSC, Ts, Ci) # Stomatal conductance to water potentials_gs_args = {'Ag': Ag, 'An': An, 'surfacic_nitrogen': surfacic_nitrogen, 'ambient_CO2': ambient_CO2, 'RH': RH, 'Ta': Ta, 'water_potential': water_potential} filtered_gs_args = {k: v for k, v in potentials_gs_args.items() if k in self.gs_args_keys} gsw = self.stomatal_model(**filtered_gs_args) # Calculation of Ci Ci = model.calculate_Ci(ambient_CO2, An, gsw) # Calculation of Ts and Tr Ts, Tr = model.organ_temperature(element_inputs['width'], element_inputs['height'], height_canopy, Ur, PARa, gsw, Ta, Ts, RH, organ_label) count += 1 if count >= 30: if abs((Ci - prec_Ci) / prec_Ci) >= parameters.DELTA_CONVERGENCE: print('{}, Ci cannot converge, prec_Ci= {}, Ci= {}'.format(organ_label, prec_Ci, Ci)) if prec_Ts != 0 and abs((Ts - prec_Ts) / prec_Ts) >= parameters.DELTA_CONVERGENCE: print('{}, Ts cannot converge, prec_Ts= {}, Ts= {}'.format(organ_label, prec_Ts, Ts)) break if abs((Ci - prec_Ci) / prec_Ci) < parameters.DELTA_CONVERGENCE and ( (prec_Ts == 0 and (Ts - prec_Ts) == 0) or abs( (Ts - prec_Ts) / prec_Ts) < parameters.DELTA_CONVERGENCE): break #: Conversion of Tr from mm s-1 to mmol m-2 s-1 (more suitable for further use of Tr) Tr = (Tr * 1E6) / parameters.MM_WATER # Using 1 mm = 1kg m-2 #: Decrease efficiency of non-lamina organs if organ_label != 'blade': Ag = Ag * parameters.EFFICENCY_STEM Ag_prim_list.append(Ag) # Aggregation of photosynthesis at element scale if not Ag_prim_list: Ag = 0 else: Ag = sum([Ag * area for Ag, area in zip(Ag_prim_list, areas_list)]) / sum(areas_list) element_outputs = {'Ag': Ag, 'An': An, 'Rd': Rd, 'Tr': Tr, 'Ts': Ts, 'gs': gsw, 'Ci': Ci, 'width': element_inputs['width'], 'height': element_inputs['height']} self.outputs[element_id] = element_outputs