Source code for openalea.metafspm.component

from dataclasses import dataclass, field, fields
from typing import Literal
import numpy as np

from .component_factory import *



[docs] def declare(unit: str, unit_comment: str, description: str, min_value: float, max_value: float, value_comment: str, references: str, DOI: list, variable_type: Literal["state_variable", "plant_scale_state", "input", "parameter"], by: str, state_variable_type: Literal["massic_concentration", "intensive", "extensive", "NonInertialExtensive", "NonInertialIntensive", "descriptor"], edit_by: Literal["user", "dev"], default=None, default_factory=None): """ Resulting from a consensus, this function is used to constrain component variables declaration in a dataclass in a commonly admitted way. :param default: Default value of the attribute if not superimposed by scenario of coupling. :param unit: International system unit :param unit_comment: More precision about unit (example : mol of ... per g of ...) :param description: Full description of the variable purpose, scale and related hypotheses. :param value_comment: Commentary if default value identified from literature has been overwritten, store original value and explain why it was changed. :param references: References for value and hypotheses (format Author et al., Year). :param DOI: DOIs of the referred papers, stored in a list of str. :param variable_type: variable type according to component standards; -input- will be read as expected input from another model. If not coupled, the model will keep this default. -state_variable- are the segment scale state variables. -plant_scale_state- are the summed totals variables or the plant scale variables. -parameter- are the model parameters. :param by: which model is provider of the considered variable? :param state_variable_type: intensive (size invariant property) or extensive (size dependant property). :param edit_by: Explicit whether the considered default value could be superimposed only by a developer or a user. This can be used to exposed more precisely certain parametrization sensitive variables for user. :raise BadDefaultError: If '(type(min_value)==float and default < min_value) or (type(max_value)==float and default > max_value)': :raise BadUnitError: If 'unit not in dir(UnitRegistry())' :raise BadVariableTypeError: If 'variable_type not in ("input", "state_variable", "plant_scale_state", "parameter")' :raise BadStateTypeError: If 'state_variable_type not in ("intensive", "extensive")' :raise BadEditError: If 'edit_by not in ("user", "developer")' """ if default_factory: return field(default_factory=default_factory, metadata=dict(unit=unit, unit_comment=unit_comment, description=description, min_value=min_value, max_value=max_value, value_comment=value_comment, references=references, DOI=DOI, variable_type=variable_type, by=by, state_variable_type=state_variable_type, edit_by=edit_by)) else: return field(default=default, metadata=dict(unit=unit, unit_comment=unit_comment, description=description, min_value=min_value, max_value=max_value, value_comment=value_comment, references=references, DOI=DOI, variable_type=variable_type, by=by, state_variable_type=state_variable_type, edit_by=edit_by))
[docs] @dataclass class Model: """ Base component for structuring base FSPM modules HYPOTHESES: self.g.properties() must have been stored self.props during child class __init__ """ choregrapher = Choregrapher() #available_inputs = [] # Will be incremented during the coupling # pullable_inputs = {} def __call__(self, *args): self.pull_available_inputs() self.choregrapher(module_family=self.__class__.__name__, *args) @property def inputs(self): return [f.name for f in fields(self) if f.metadata["variable_type"] == "input"] @property def state_variables(self): return [f.name for f in fields(self) if f.metadata["variable_type"] == "state_variable"] @property def extensive_variables(self): return [f.name for f in fields(self) if (f.metadata["variable_type"] == "state_variable" and f.metadata["state_variable_type"] == "extensive")] @property def massic_concentration(self): return [f.name for f in fields(self) if (f.metadata["variable_type"] == "state_variable" and f.metadata["state_variable_type"] == "massic_concentration")] @property def intensive_variables(self): return [f.name for f in fields(self) if (f.metadata["variable_type"] == "state_variable" and f.metadata["state_variable_type"] == "intensive")] @property def non_inertial_extensive(self): return [f.name for f in fields(self) if (f.metadata["variable_type"] == "state_variable" and f.metadata["state_variable_type"] == "NonInertialExtensive")] @property def non_inertial_intensive(self): return [f.name for f in fields(self) if (f.metadata["variable_type"] == "state_variable" and f.metadata["state_variable_type"] == "NonInertialIntensive")] @property def non_inertial_variables(self): return [f.name for f in fields(self) if (f.metadata["variable_type"] == "state_variable" and f.metadata["state_variable_type"] in ("NonInertialIntensive", "NonInertialExtensive"))] @property def descriptor(self): return [f.name for f in fields(self) if (f.metadata["variable_type"] == "state_variable" and f.metadata["state_variable_type"] == "descriptor")] @property def plant_scale_state(self): return [f.name for f in fields(self) if f.metadata["variable_type"] == "plant_scale_state"] @property def parameter(self): return [f.name for f in fields(self) if f.metadata["variable_type"] == "parameter"]
[docs] def apply_scenario(self, **kwargs): """ Method to superimpose default parameters in order to create a scenario. Use Model.documentation to discover model parameters and state variables. :param kwargs: mapping of existing variable to superimpose. """ for changed_parameter, value in kwargs.items(): if changed_parameter in dir(self): setattr(self, changed_parameter, value)
def link_self_to_mtg(self, ignore=[]): # for input variables, initialize homogeneous values on each vertices. # This behavior will be overwritten in case of module providing the input variable for name in self.inputs: if not (name in self.props.keys() and len(self.props[name]) == len(self.vertices)): # if it is not provided by mtg file, Use by default value everywhere self.props.setdefault(name, {}) self.props[name].update({key: getattr(self, name) for key in self.vertices}) # for segment scale state variables for name in self.state_variables: if name not in ignore: self.props.setdefault(name, {}) # set default in mtg, state_variable prevail on inputs self.props[name].update({key: getattr(self, name) for key in self.vertices}) # for plant scale state variables for name in self.plant_scale_state: self.props.setdefault(name, {}) # set default in mtg, state_variable prevail on inputs self.props[name].update({1: getattr(self, name)}) def pull_available_inputs(self): props = self.props for input, source_variables in self.pullable_inputs.items(): vertices = props[list(source_variables.keys())[0]].keys() props[input].update({vid: sum([props[variable][vid]*unit_conversion for variable, unit_conversion in source_variables.items()]) for vid in vertices})
[docs] def temperature_modification_old(self, soil_temperature=15, process_at_T_ref=1., T_ref=0., A=-0.05, B=3., C=1.): """ This function calculates how the value of a process should be modified according to soil temperature (in degrees Celsius). Parameters correspond to the value of the process at reference temperature T_ref (process_at_T_ref), to two empirical coefficients A and B, and to a coefficient C used to switch between different formalisms. If C=0 and B=1, then the relationship corresponds to a classical linear increase with temperature (thermal time). If C=1, A=0 and B>1, then the relationship corresponds to a classical exponential increase with temperature (Q10). If C=1, A<0 and B>0, then the relationship corresponds to bell-shaped curve, close to the one from Parent et al. (2010). :param T_ref: the reference temperature :param A: parameter A (may be equivalent to the coefficient of linear increase) :param B: parameter B (may be equivalent to the Q10 value) :param C: parameter C (either 0 or 1) :return: the new value of the process """ # We avoid unwanted cases: if C != 0 and C != 1: print("The modification of the process at T =", soil_temperature, "only works for C=0 or C=1!") print("The modified process has been set to 0.") return 0. elif C == 1: if (A * (soil_temperature - T_ref) + B) < 0.: print("The modification of the process at T =", soil_temperature, "is unstable with this set of parameters!") print("The modified process has been set to 0.") modified_process = 0. return modified_process # We compute a temperature-modified process, correspond to a Q10-modified relationship, # based on the work of Tjoelker et al. (2001): modified_process = process_at_T_ref * (A * (soil_temperature - T_ref) + B) ** (1 - C) \ * (A * (soil_temperature - T_ref) + B) ** ( C * (soil_temperature - T_ref) / 10.) return max(modified_process, 0.)
[docs] def temperature_modification(self, soil_temperature=15, process_at_T_ref=1., T_ref=0., A=-0.05, B=3., C=1.): """ This function calculates how the value of a process should be modified according to soil temperature (in degrees Celsius). Parameters correspond to the value of the process at reference temperature T_ref (process_at_T_ref), to two empirical coefficients A and B, and to a coefficient C used to switch between different formalisms. If C=0 and B=1, then the relationship corresponds to a classical linear increase with temperature (thermal time). If C=1, A=0 and B>1, then the relationship corresponds to a classical exponential increase with temperature (Q10). If C=1, A<0 and B>0, then the relationship corresponds to bell-shaped curve, close to the one from Parent et al. (2010). :param T_ref: the reference temperature :param A: parameter A (may be equivalent to the coefficient of linear increase) :param B: parameter B (may be equivalent to the Q10 value) :param C: parameter C (either 0 or 1) :return: the new value of the process """ # We compute a temperature-modified process, correspond to a Q10-modified relationship, # based on the work of Tjoelker et al. (2001): modified_process = process_at_T_ref * (A * (soil_temperature - T_ref) + B) ** (1 - C) \ * (A * (soil_temperature - T_ref) + B) ** ( C * (soil_temperature - T_ref) / 10.) return np.where(((C != 0) & (C != 1)) | ((C == 1) & ((A * (soil_temperature - T_ref) + B) < 0.)), 0., np.maximum(modified_process, 0.))