Source code for openalea.fspmwheat.caribu_facade

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

import pandas as pd
import numpy as np
import warnings

from openalea.caribu.CaribuScene import CaribuScene
from openalea.caribu.sky_tools import GenSky, GetLight, Gensun, GetLightsSun, spitters_horaire

from openalea.fspmwheat import tools

"""
    fspmwheat.caribu_facade
    ~~~~~~~~~~~~~~~~~~~~~~~

    The module :mod:`fspmwheat.caribu_facade` is a facade of the model Caribu.

    This module permits to initialize and run the model Caribu from a :class:`MTG <openalea.mtg.mtg.MTG>`
    in a convenient and transparent way, wrapping all the internal complexity of the model, and dealing
    with all the tedious initialization and conversion processes.

"""

#: the columns which define the topology in the elements scale dataframe shared between all models
SHARED_ELEMENTS_INPUTS_OUTPUTS_INDEXES = ['plant', 'axis', 'metamer', 'organ', 'element']

#: the outputs of Caribu
CARIBU_OUTPUTS = ['PARa', 'Erel', 'PARa_prim', 'Erel_prim']


[docs] class CaribuFacade(object): """ The CaribuFacade class permits to initialize, run the model Caribu from a :class:`MTG <openalea.mtg.mtg.MTG>`, and update the MTG and the dataframes shared between all models. Use :meth:`run` to run the model. """ def __init__(self, shared_mtg, shared_elements_inputs_outputs_df, geometrical_model, update_shared_df=True): """ :param openalea.mtg.MTG shared_mtg: The MTG shared between all models. :param pandas.DataFrame shared_elements_inputs_outputs_df: The dataframe of inputs and outputs at elements scale shared between all models. :param openalea.adel.adel_dynamic.AdelWheatDyn geometrical_model: The model which deals with geometry. This model must have an attribute "domain". :param bool update_shared_df: If `True` update the shared dataframes at init and at each run (unless stated otherwise) """ self._shared_mtg = shared_mtg #: the MTG shared between all models self._shared_elements_inputs_outputs_df = shared_elements_inputs_outputs_df #: the dataframe at elements scale shared between all models self._geometrical_model = geometrical_model #: the model which deals with geometry self._alea_canopy = pd.DataFrame() #: alea table to generate the heterogeneous canopy self._update_shared_df = update_shared_df
[docs] def run(self, run_caribu, sun_sky_option='mix', energy=1, DOY=1, hourTU=12, latitude=48.85, diffuse_model='soc', azimuts=4, zenits=5, heterogeneous_canopy=False, plant_density=250., inter_row=0.15, update_shared_df=None, prim_scale=False): """ Run the model and update the MTG and the dataframes shared between all models. :param bool run_caribu: If 'True', run the CARIBU model to calculate light distribution inside the 3D canopy. :param str sun_sky_option: The irradiance model, should be one of 'mix' or 'sun' or 'sky' :param float energy: The incident PAR above the canopy (µmol m-2 s-1) :param int DOY: Day Of the Year to be used for solar sources :param int hourTU: Hour to be used for solar sources (Universal Time) :param float latitude: latitude to be used for solar sources (°) :param string diffuse_model: The kind of diffuse model, either 'soc' or 'uoc'. :param int azimuts: The number of azimutal positions. :param int zenits: The number of zenital positions. :param bool heterogeneous_canopy: Whether to create a duplicated heterogeneous canopy from the initial mtg. :param float plant_density: Number of plant per m2 in the stand (plant m-2). :param float inter_row: Inter-row spacing in the stand (m). :param bool update_shared_df: if 'True', update the shared dataframes at this time step. :param bool prim_scale: If True, light distribution output at primitive scale, if not at organ scale """ c_scene_sky, c_scene_sun, Erel_input, Erel_input_prim = self._initialize_model(run_caribu, 1, diffuse_model, azimuts, zenits, DOY, hourTU, latitude, heterogeneous_canopy, plant_density, inter_row) outputs = {} if run_caribu: #: Diffuse light if sun_sky_option == 'sky': raw, aggregated_sky = c_scene_sky.run(direct=True, infinite=True) Erel_sky = aggregated_sky['par']['Eabs'] #: Erel is the relative surfacic absorbed energy per organ PARa_sky = {k: v * energy for k, v in Erel_sky.items()} Erel_output = Erel_sky PARa_output = PARa_sky # Primitive scale if prim_scale: Erel_prim = raw['par']['Eabs'] raw_Eabs_abs = {k: [Eabs * energy for Eabs in raw['par']['Eabs'][k]] for k in raw['par']['Eabs']} outputs.update({'Erel_prim': Erel_prim, 'PARa_prim': raw_Eabs_abs, 'area_prim': raw['par']['area']}) #: Direct light elif sun_sky_option == 'sun': raw, aggregated_sun = c_scene_sun.run(direct=True, infinite=True) Erel_sun = aggregated_sun['par']['Eabs'] #: Erel is the relative surfacic absorbed energy per organ PARa_sun = {k: v * energy for k, v in Erel_sun.items()} Erel_output = Erel_sun PARa_output = PARa_sun # Primitive scale if prim_scale: Erel_prim = raw['par']['Eabs'] raw_Eabs_abs = {k: [Eabs * energy for Eabs in raw['par']['Eabs'][k]] for k in raw['par']['Eabs']} outputs.update({'Erel_prim': Erel_prim, 'PARa_prim': raw_Eabs_abs, 'area_prim': raw['par']['area']}) #: Mix sky-Sun elif sun_sky_option == 'mix': #: Diffuse raw_sky, aggregated_sky = c_scene_sky.run(direct=True, infinite=True) Erel_sky = aggregated_sky['par']['Eabs'] #: Direct raw_sun, aggregated_sun = c_scene_sun.run(direct=True, infinite=True) Erel_sun = aggregated_sun['par']['Eabs'] #: Spitters's model estimating for the diffuse:direct ratio Rg = energy / 2.02 #: Global Radiation (W.m-2) RdRs = spitters_horaire.RdRsH(Rg=Rg, DOY=DOY, heureTU=hourTU, latitude=latitude) #: Diffuse fraction of the global irradiance Erel = {} for element_id, Erel_value in Erel_sky.items(): Erel[element_id] = RdRs * Erel_value + (1 - RdRs) * Erel_sun[element_id] Erel_output = Erel PARa_output = {k: v * energy for k, v in Erel_output.items()} # Primitive scale if prim_scale: raise ValueError("prim_scale not yet implemented for mix sun_sky_option.") else: raise ValueError("Unknown sun_sky_option : can be either 'mix', 'sun' or 'sky'.") # Ouputs outputs.update({'PARa': PARa_output, 'Erel': Erel_output}) else: PARa_output = {k: v * energy for k, v in Erel_input.items()} raw_Eabs_abs = {k: [Eabs * energy for Eabs in Erel_input_prim[k]] for k in Erel_input_prim} outputs.update({'PARa': PARa_output}) # Primitive scale if prim_scale: outputs.update({'PARa_prim': raw_Eabs_abs}) # Updates self.update_shared_MTG(outputs) if update_shared_df or (update_shared_df is None and self._update_shared_df): self.update_shared_dataframes(outputs)
def _initialize_model(self, run_caribu, energy, diffuse_model, azimuts, zenits, DOY, hourTU, latitude, heterogeneous_canopy, plant_density, inter_row): """ Initialize the inputs of the model from the MTG shared :param bool run_caribu: If 'True', run the CARIBU model to calculate light distribution inside the 3D canopy. :param float energy: The incident PAR above the canopy (µmol m-2 s-1) :param string diffuse_model: The kind of diffuse model, either 'soc' or 'uoc'. :param int azimuts: The number of azimutal positions. :param int zenits: The number of zenital positions. :param int DOY: Day Of the Year to be used for solar sources :param int hourTU: Hour to be used for solar sources (Universal Time) :param float latitude: latitude to be used for solar sources (°) :param bool heterogeneous_canopy: Whether to create a duplicated heterogeneous canopy from the initial mtg. :return: A tuple of Caribu scenes instantiated for sky and sun sources, respectively, and two dictionaries with Erel value per vertex id and per primitive. :rtype: (CaribuScene, CaribuScene, dict, dict) """ c_scene_sky, c_scene_sun, Erel, Erel_prim = None, None, None, None if run_caribu: #: Diffuse light sources : Get the energy and positions of the source for each sector as a string sky_string = GetLight.GetLight(GenSky.GenSky()(energy, diffuse_model, azimuts, zenits)) #: (Energy, soc/uoc, azimuts, zenits) # Convert string to list in order to be compatible with CaribuScene input format sky = [] for string in sky_string.split('\n'): if len(string) != 0: string_split = string.split(' ') t = tuple((float(string_split[0]), tuple((float(string_split[1]), float(string_split[2]), float(string_split[3]))))) sky.append(t) #: Direct light sources (sun positions) sun = Gensun.Gensun()(energy, DOY, hourTU, latitude) sun = GetLightsSun.GetLightsSun(sun) sun_str_split = sun.split(' ') sun = [tuple((float(sun_str_split[0]), tuple((float(sun_str_split[1]), float(sun_str_split[2]), float(sun_str_split[3])))))] #: Optical properties opt = {'par': {}} geom = self._shared_mtg.property('geometry') for vid in geom.keys(): if self._shared_mtg.class_name(vid) in ('LeafElement1', 'LeafElement'): opt['par'][vid] = (0.10, 0.05) #: (reflectance, transmittance) of the adaxial side of the leaves elif self._shared_mtg.class_name(vid) == 'StemElement': opt['par'][vid] = (0.10,) #: (reflectance,) of the stems else: warnings.warn('Warning: unknown element type {}, vid={}'.format(self._shared_mtg.class_name(vid), vid)) #: Generates CaribuScenes if not heterogeneous_canopy: # TODO: adapt the domain to plant_density c_scene_sky = CaribuScene(scene=self._shared_mtg, light=sky, pattern=self._geometrical_model.domain, opt=opt) c_scene_sun = CaribuScene(scene=self._shared_mtg, light=sun, pattern=self._geometrical_model.domain, opt=opt) else: duplicated_scene, domain = self._create_heterogeneous_canopy(plant_density=plant_density, inter_row=inter_row) c_scene_sky = CaribuScene(scene=duplicated_scene, light=sky, pattern=domain, opt=opt) c_scene_sun = CaribuScene(scene=duplicated_scene, light=sun, pattern=domain, opt=opt) else: Erel = self._shared_mtg.property('Erel') Erel_prim = self._shared_mtg.property('Erel_prim') return c_scene_sky, c_scene_sun, Erel, Erel_prim def _create_heterogeneous_canopy(self, nplants=50, var_plant_position=0.03, var_leaf_inclination=0.157, var_leaf_azimut=1.57, var_stem_azimut=0.157, plant_density=250, inter_row=0.15): """ Duplicate a plant in order to obtain a heterogeneous canopy. :param int nplants: the desired number of duplicated plants :param float var_plant_position: variability for plant position (m) :param float var_leaf_inclination: variability for leaf inclination (rad) :param float var_leaf_azimut: variability for leaf azimut (rad) :param float var_stem_azimut: variability for stem azimut (rad) :return: duplicated heterogenous scene and its domain :rtype: openalea.plantgl.all.Scene, (float) """ from openalea.adel.Stand import AgronomicStand import openalea.plantgl.all as plantgl import random # Load scene initial_scene = self._geometrical_model.scene(self._shared_mtg) # Planter stand = AgronomicStand(sowing_density=plant_density, plant_density=plant_density, inter_row=inter_row, noise=var_plant_position) _, domain, positions, _ = stand.smart_stand(nplants=nplants, at=inter_row, convunit=1) random.seed(1234) # Built alea table if does not exist yet if self._alea_canopy.empty: elements_vid_list = [] for mtg_plant_vid in self._shared_mtg.components_iter(self._shared_mtg.root): for mtg_axis_vid in self._shared_mtg.components_iter(mtg_plant_vid): for mtg_metamer_vid in self._shared_mtg.components_iter(mtg_axis_vid): for mtg_organ_vid in self._shared_mtg.components_iter(mtg_metamer_vid): for mtg_element_vid in self._shared_mtg.components_iter(mtg_organ_vid): if self._shared_mtg.label(mtg_element_vid) == 'LeafElement1': elements_vid_list.append(mtg_element_vid) elements_vid_df = pd.DataFrame({'vid': elements_vid_list, 'tmp': 1}) positions_df = pd.DataFrame({'pos': range(len(positions)), 'tmp': 1, 'azimut_leaf': 0., 'inclination_leaf': 0.}) alea = pd.merge(elements_vid_df, positions_df, on=['tmp']) alea = alea.drop('tmp', axis=1) for vid in elements_vid_list: np.random.seed(vid) alea.loc[alea['vid'] == vid, 'azimut_leaf'] = np.random.uniform(-var_leaf_azimut, var_leaf_azimut, size=len(positions)) alea.loc[alea['vid'] == vid, 'inclination_leaf'] = np.random.uniform(-var_leaf_inclination, var_leaf_inclination, size=len(positions)) self._alea_canopy = alea # Duplication and heterogeneity duplicated_scene = plantgl.Scene() position_number = 0 for pos in positions: azimut_stem = random.uniform(-var_stem_azimut, var_stem_azimut) for shp in initial_scene: if self._shared_mtg.label(shp.id) == 'StemElement': rotated_geometry = plantgl.EulerRotated(azimut_stem, 0, 0, shp.geometry) translated_geometry = plantgl.Translated(plantgl.Vector3(pos), rotated_geometry) new_shape = plantgl.Shape(translated_geometry, appearance=shp.appearance, id=shp.id) duplicated_scene += new_shape elif self._shared_mtg.label(shp.id) == 'LeafElement1': # Add shp.id in alea_canopy if not in yet: if shp.id not in list(self._alea_canopy['vid']): new_vid_df = pd.DataFrame({'vid': shp.id, 'pos': range(len(positions))}) np.random.seed(shp.id) new_vid_df['azimut_leaf'] = np.random.uniform(-var_leaf_azimut, var_leaf_azimut, size=len(positions)) new_vid_df['inclination_leaf'] = np.random.uniform(-var_leaf_inclination, var_leaf_inclination, size=len(positions)) self._alea_canopy = pd.concat([self._alea_canopy, new_vid_df], ignore_index=True) # Translation to origin anchor_point = self._shared_mtg.get_vertex_property(shp.id)['anchor_point'] trans_to_origin = plantgl.Translated(-anchor_point, shp.geometry) # Rotation variability azimut = self._alea_canopy.loc[(self._alea_canopy.pos == position_number) & (self._alea_canopy.vid == shp.id), 'azimut_leaf'].values[0] inclination = self._alea_canopy.loc[(self._alea_canopy.pos == position_number) & (self._alea_canopy.vid == shp.id), 'inclination_leaf'].values[0] rotated_geometry = plantgl.EulerRotated(azimut, inclination, 0, trans_to_origin) # Restore leaf base at initial anchor point translated_geometry = plantgl.Translated(anchor_point, rotated_geometry) # Translate leaf to new plant position translated_geometry = plantgl.Translated(pos, translated_geometry) new_shape = plantgl.Shape(translated_geometry, appearance=shp.appearance, id=shp.id) duplicated_scene += new_shape position_number += 1 return duplicated_scene, domain
[docs] def update_shared_MTG(self, aggregated_outputs): """ Update the MTG shared between all models from the population of Caribu. :param dict aggregated_outputs: {'param1': { vid1: , vid2, ...}, 'param2': { vid1: , vid2, ...}} """ # add the missing property for param in aggregated_outputs.keys(): if param not in self._shared_mtg.properties(): self._shared_mtg.add_property(param) # update the MTG self._shared_mtg.property(param).update(aggregated_outputs[param])
[docs] def update_shared_dataframes(self, aggregated_outputs): """ Update the dataframes shared between all models from the inputs dataframes or the outputs dataframes of the model. :param dict aggregated_outputs: {'param1': { vid1: , vid2, ...}, 'param2': { vid1: , vid2, ...}} """ ids = [] ids_lidt_built = False aggregated_outputs_list = {} for param in aggregated_outputs.keys(): aggregated_outputs_list[param] = [] for vid in sorted(aggregated_outputs[param].keys()): if not ids_lidt_built: ind = int(self._shared_mtg.index(self._shared_mtg.complex_at_scale(vid, 1))), self._shared_mtg.label(self._shared_mtg.complex_at_scale(vid, 2)), \ int(self._shared_mtg.index(self._shared_mtg.complex_at_scale(vid, 3))), self._shared_mtg.label(self._shared_mtg.complex_at_scale(vid, 4)), self._shared_mtg.label(vid) ids.append(ind) aggregated_outputs_list[param].append(aggregated_outputs[param][vid]) ids_lidt_built = True ids_df = pd.DataFrame(ids, columns=SHARED_ELEMENTS_INPUTS_OUTPUTS_INDEXES) data_df = pd.DataFrame(aggregated_outputs_list) df = pd.concat([ids_df, data_df], axis=1) df.sort_values(['plant', 'axis', 'metamer', 'organ', 'element'], inplace=True) tools.combine_dataframes_inplace(df, SHARED_ELEMENTS_INPUTS_OUTPUTS_INDEXES, self._shared_elements_inputs_outputs_df)