# -*- coding: latin-1 -*-
import datetime
import os
import random
import time
import warnings
import ast
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from openalea.adel.adel_dynamic import AdelDyn
from openalea.adel.Stand import AgronomicStand
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
"""
Executes the coupling between models CN-Metabolism, Gas-Exchange, Senescence, Morphogenesis, Growth, Adel-Wheat and Caribu.
This uses the format MTG to exchange data between the models.
"""
random.seed(1234)
np.random.seed(1234)
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']
[docs]
def get_management_value(df, variable_name):
"""
Extracts management variables from the input file.
:param pandas.DataFrame df: the dataframe with all management variables
:param str variable_name: name of the current management variable
return management_input
:rtype float or dict
"""
management_input = df.loc[variable_name].iloc[0]
# In case of missing value in the management file
if pd.isna(management_input):
raise ValueError('No input found for {} in the management file.'.format(variable_name))
management_input = ast.literal_eval(str(management_input))
return management_input
[docs]
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))
[docs]
def run(simulation_length, forced_start_time=0, run_simu=True, run_postprocessing=True, generate_graphs=True,
run_from_outputs=False, stored_times=None, show_3Dplant=False,
hydraulics=False, stomatal_model_name='BWB', drought_trigger=None, rehydration_scenario=None,
optimal_growth_option=False, option_static=False,
external_soil_model=False, tillers_replications=None, heterogeneous_canopy=True,
update_parameters_all_models=None, step_callback=None,
INPUTS_DIRPATH='inputs', METEO_FILENAME='meteo.csv', MANAGEMENT_FILENAME='management.csv',
OUTPUTS_DIRPATH='outputs', POSTPROCESSING_DIRPATH='postprocessing', GRAPHS_DIRPATH='graphs'):
"""
Run a simulation of integration with coupling to several models
:param int simulation_length: length of the simulation (hours)
:param int forced_start_time: desired start time (hour)
:param bool run_simu: whether to run the simulation
:param bool run_postprocessing: whether to run the postprocessing
:param bool generate_graphs: whether to run generate graphs
:param bool run_from_outputs: whether to start a simulation from a specific time and initial states as found in previous outputs
:param str or list stored_times: Time steps when are stored the model outputs. Can be either 'all', a list or an empty list. Default to 'all'
:param bool show_3Dplant: whether to plot the scene in pgl viewer
:param bool hydraulics: if True the model will assume the coupling to the turgor-driven growth model
:param str stomatal_model_name: the model of stomatal conductance. Should be one of 'BWB', 'Leuning', 'Tuzet' or 'hydraulics'. 'Tuzet' and 'hydraulics' requires hydraulics to be True
:param dict or None drought_trigger: a dict for external drought control scenario.
{'trigger_variable': value}. For now, only implemented for 'green_area' variable (value = green area above which the drought will be triggered).
:param dict or None rehydration_scenario: a dict to specify the rehydration scenario.
{'stop_drought_SRWC': SRWC at which the drought event stops (%),
'SRWC_target': Target SRWC for rehydration (%),
'rehydration_duration': duration of the rehydration period (days)}
:param bool optimal_growth_option: if True the model will assume optimal growth conditions
:param bool option_static: Whether the model should be run for a static plant architecture
:param bool external_soil_model: whether an external soil model is coupled to cnmetabolism. If True, cnmetabolism will skip calculations made in soil and uptake N by roots
:param dict [str, float] tillers_replications: a dictionary with tiller id as key, and weight of replication as value.
:param bool heterogeneous_canopy: Whether to create a duplicated heterogeneous canopy from the initial mtg.
:param dict update_parameters_all_models: a dict to update model parameters
{'cnmetabolism': {'organ1': {'param1': 'val1', 'param2': 'val2'},
'organ2': {'param1': 'val1', 'param2': 'val2'}
},
'morphogenesis': {'param1': 'val1', 'param2': 'val2'}
}
:param dict or None step_callback: a dict of functions used to force some external inputs that are natively computed by the model
{'function_name' : function , ...}
:param str INPUTS_DIRPATH: the path directory of inputs
# The directory at path 'adel' must contain files 'adel_pars.RData', 'adel0000.pckl' and 'scene0000.bgeom' for ADELWHEAT
:param str METEO_FILENAME: the name of the file with meteo data
:param str MANAGEMENT_FILENAME: the name of the file with managment data
:param str OUTPUTS_DIRPATH: the path to save outputs
:param str POSTPROCESSING_DIRPATH: the path to save postprocessings
:param str GRAPHS_DIRPATH: the path to save graphs
"""
# ---------------------------------------------
# ----- CONFIGURATION OF THE SIMULATION -------
# ---------------------------------------------
# -- SIMULATION PARAMETERS --
# Length of the simulation (in hours)
SIMULATION_LENGTH = simulation_length
# define the time step in hours for each simulator
CARIBU_TIMESTEP = 4
SENESCENCE_TIMESTEP = 1
MORPHOGENESIS_TIMESTEP = 1
GROWTH_TIMESTEP = 1
CNMETABOLISM_TIMESTEP = 1
hydraulics_TIMESTEP = 1
# precision of floats used to write and format the output CSV files
OUTPUTS_PRECISION = 8
# number of seconds in 1 hour
HOUR_TO_SECOND_CONVERSION_FACTOR = 3600
# 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'
# -- INPUTS CONFIGURATION --
# Path of the directory which contains the inputs of the model
INPUTS_DIRPATH = INPUTS_DIRPATH
# 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'
# 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)
# Name of the CSV files which contains the meteo data
meteo = pd.read_csv(os.path.join(INPUTS_DIRPATH, METEO_FILENAME), index_col='t')
# 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)
plant_density = management_variables.get('plant_density', {1: 250})
inter_row = management_variables.get('inter_row', 0.15)
Zsowing = management_variables.get('sowing_depth', 0.025)
N_fertilizations = management_variables.get('N_fertilizations', {})
# -- OUTPUTS CONFIGURATION --
# 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 isinstance(stored_times, list)):
print('stored_times should be either \'all\', a list or an empty list.')
raise
# create empty dataframes to shared data between the models
shared_axes_inputs_outputs_df = pd.DataFrame()
shared_organs_inputs_outputs_df = pd.DataFrame()
shared_hiddenzones_inputs_outputs_df = pd.DataFrame()
shared_elements_inputs_outputs_df = pd.DataFrame()
shared_soils_inputs_outputs_df = pd.DataFrame()
# define lists of dataframes to store the inputs and the outputs of the models at each step.
axes_all_data_list = []
organs_all_data_list = [] # organs which belong to the axes: roots, phloem, grains
hiddenzones_all_data_list = []
elements_all_data_list = []
soils_all_data_list = []
all_simulation_steps = [] # to store the steps of the simulation
# -- POSTPROCESSING CONFIGURATION --
# Name of the CSV files which will contain the postprocessing of the model
AXES_POSTPROCESSING_FILENAME = 'axes_postprocessing.csv'
ORGANS_POSTPROCESSING_FILENAME = 'organs_postprocessing.csv'
HIDDENZONES_POSTPROCESSING_FILENAME = 'hiddenzones_postprocessing.csv'
ELEMENTS_POSTPROCESSING_FILENAME = 'elements_postprocessing.csv'
SOILS_POSTPROCESSING_FILENAME = 'soils_postprocessing.csv'
# -- ADEL and MTG CONFIGURATION --
# Create the stand using density pattern
stand = AgronomicStand(sowing_density=plant_density[1], plant_density=plant_density[1], inter_row=inter_row, noise=0.) #todo to be adapted if multiple cultivars
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']
g = step_callback['ADEL_mtg'](adel_wheat, INPUTS_DIRPATH, nff) # Create a new MTG
else:
g = adel_wheat.load(directory=INPUTS_DIRPATH) # read adelwheat inputs at t0 from a serialised MTG
# ---------------------------------------------
# ----- CONFIGURATION OF THE FACADES -------
# ---------------------------------------------
# -- MORPHOGENESIS (created first because it is the only facade to add new metamers) --
# Initial states
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 'morphogenesis' in update_parameters_all_models:
update_parameters_morphogenesis = update_parameters_all_models['morphogenesis']
else:
update_parameters_morphogenesis = None
# Facade initialisation
morphogenesis_facade_ = morphogenesis_facade.MorphogenesisFacade(g,
MORPHOGENESIS_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR,
morphogenesis_axes_initial_state,
morphogenesis_hiddenzones_initial_state,
morphogenesis_elements_initial_state,
shared_axes_inputs_outputs_df,
shared_hiddenzones_inputs_outputs_df,
shared_elements_inputs_outputs_df,
adel_wheat, phytoT_df,
hydraulics=hydraulics,
optimal_growth_option=optimal_growth_option,
option_static=option_static,
update_parameters=update_parameters_morphogenesis,
update_shared_df=UPDATE_SHARED_DF)
# -- CARIBU --
caribu_facade_ = caribu_facade.CaribuFacade(g,
shared_elements_inputs_outputs_df,
adel_wheat,
update_shared_df=UPDATE_SHARED_DF)
# -- SENESCENCE --
# Initial states
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()
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()
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 'senescence' in update_parameters_all_models:
update_parameters_senescence = update_parameters_all_models['senescence']
else:
update_parameters_senescence = None
# Facade initialisation
senescence_facade_ = senescence_facade.SENESCENCEFacade(g,
SENESCENCE_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR,
senescence_roots_initial_state,
senescence_axes_initial_state,
senescence_elements_initial_state,
shared_organs_inputs_outputs_df,
shared_axes_inputs_outputs_df,
shared_elements_inputs_outputs_df,
update_parameters=update_parameters_senescence,
update_shared_df=UPDATE_SHARED_DF)
# -- GAS-EXCHANGE --
# Initial states
gasexchange_elements_initial_state = inputs_dataframes[ELEMENTS_INITIAL_STATE_FILENAME]
gasexchange_axes_initial_state = inputs_dataframes[AXES_INITIAL_STATE_FILENAME]
# 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
gasexchange_facade_ = gasexchange_facade.GasExchangeFacade(g,
gasexchange_elements_initial_state,
gasexchange_axes_initial_state,
shared_elements_inputs_outputs_df,
stomatal_model_name=stomatal_model_name,
hydraulics=hydraulics,
update_parameters=update_parameters_gasexchange,
update_shared_df=UPDATE_SHARED_DF)
# -- 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 'growth' in update_parameters_all_models:
update_parameters_growth = update_parameters_all_models['growth']
else:
update_parameters_growth = None
# Facade initialisation
growth_facade_ = growth_facade.GrowthFacade(g,
GROWTH_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR,
growth_hiddenzones_initial_state,
growth_elements_initial_state,
growth_root_initial_state,
growth_axes_initial_state,
shared_organs_inputs_outputs_df,
shared_hiddenzones_inputs_outputs_df,
shared_elements_inputs_outputs_df,
shared_axes_inputs_outputs_df,
hydraulics=hydraulics,
update_parameters=update_parameters_growth,
update_shared_df=UPDATE_SHARED_DF)
# -- 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()
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()
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()
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 'cnmetabolism' in update_parameters_all_models:
update_parameters_cnmetabolism = update_parameters_all_models['cnmetabolism']
else:
update_parameters_cnmetabolism = {}
# Facade initialisation
cnmetabolism_facade_ = cnmetabolism_facade.CNMetabolismFacade(g,
CNMETABOLISM_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR,
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,
shared_axes_inputs_outputs_df,
shared_organs_inputs_outputs_df,
shared_hiddenzones_inputs_outputs_df,
shared_elements_inputs_outputs_df,
shared_soils_inputs_outputs_df,
tillers_replications=tillers_replications,
external_soil_model=external_soil_model,
update_shared_df=UPDATE_SHARED_DF)
# -- 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)
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
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,
shared_axes_inputs_outputs_df,
shared_hiddenzones_inputs_outputs_df,
shared_elements_inputs_outputs_df,
shared_organs_inputs_outputs_df,
shared_soils_inputs_outputs_df,
update_shared_df=UPDATE_SHARED_DF)
# Run cnmetabolism with constant nitrates concentration in the soil if specified
if 'constant_Conc_Nitrates' in N_fertilizations.keys():
cnmetabolism_facade_.soils[(1, 'MS')].constant_Conc_Nitrates = True # TODO: make (1, 'MS') more general
cnmetabolism_facade_.soils[(1, 'MS')].nitrates = N_fertilizations['constant_Conc_Nitrates'] * 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, cnmetabolism_facade_.population, g)
except KeyError:
print('Function name error in step_callback keys. It should be nitrate_uptake')
# -- MODEL INTEGRATION --
# Facade initialisation
build_outputs_ = build_outputs.BuildOutputs(g, morphogenesis_facade_, growth_facade_, gasexchange_facade_, hydraulics_facade_)
# Update geometry
adel_wheat.update_geometry(g)
if show_3Dplant:
adel_wheat.plot(g)
# ---------------------------------------------
# ----- RUN OF THE SIMULATION -------
# ---------------------------------------------
if run_simu:
try:
current_time_of_the_system = time.time()
for t in range(START_TIME, SIMULATION_LENGTH, 1):
print('t cnmetabolism is {}'.format(t))
# if t == 1 or t >= 800:
# adel_wheat.scene(g).save('toto_{}.bgeom'.format(t))
# run Caribu
PARi = meteo.loc[t, ['PARi']].iloc[0]
DOY = meteo.loc[t, ['DOY']].iloc[0]
hour = meteo.loc[t, ['hour']].iloc[0]
PARi_next_hours = meteo.loc[range(t, t + CARIBU_TIMESTEP), ['PARi']].sum().values[0]
if (t % CARIBU_TIMESTEP == 0) and (PARi_next_hours > 0) and bool(g.property('geometry')):
run_caribu = True
else:
run_caribu = False
caribu_facade_.run(run_caribu, energy=PARi, DOY=DOY, hourTU=hour, latitude=48.85, sun_sky_option='sky',
heterogeneous_canopy=heterogeneous_canopy, plant_density=plant_density[1], inter_row=inter_row)
# run Senescence
senescence_facade_.run()
# Test for dead plant # TODO: adapt in case of multiple plants
if not shared_elements_inputs_outputs_df.empty and \
np.nansum(shared_elements_inputs_outputs_df.loc[shared_elements_inputs_outputs_df['element'].isin(['StemElement', 'LeafElement1']), 'green_area']) == 0:
# append the inputs and outputs at current step to global lists
all_simulation_steps.append(t)
axes_all_data_list.append(shared_axes_inputs_outputs_df.copy())
organs_all_data_list.append(shared_organs_inputs_outputs_df.copy())
hiddenzones_all_data_list.append(shared_hiddenzones_inputs_outputs_df.copy())
elements_all_data_list.append(shared_elements_inputs_outputs_df.copy())
soils_all_data_list.append(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, Tsoil, ambient_CO2, RH, Ur = meteo.loc[t, ['air_temperature', 'soil_temperature', 'ambient_CO2', 'humidity', 'Wind']]
# run Gas-Exchange
gasexchange_facade_.run(Ta, ambient_CO2, RH, Ur)
# run Morphogenesis
Tair, Tsoil = meteo.loc[t, ['air_temperature', 'soil_temperature']]
morphogenesis_facade_.run(Tair, Tsoil, Zsowing)
# Update geometry
adel_wheat.update_geometry(g)
if show_3Dplant:
adel_wheat.plot(g)
# run hydraulics
if hydraulics and hydraulics_facade_ is not None:
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
hydraulics_facade_.run()
# Update geometry
adel_wheat.update_geometry(g)
if show_3Dplant:
adel_wheat.plot(g)
# adel_wheat.scene(g).save(r'adel_save\t{}.bgeom'.format(t))
# run Growth
growth_facade_.run()
# run cnmetabolism
# N fertilization if any
if t in N_fertilizations.keys():
cnmetabolism_facade_.soils[(1, 'MS')].nitrates += N_fertilizations[t]
# Force root nitrate uptake if specified
if external_soil_model and step_callback is not None:
try:
step_callback['nitrate_uptake'](t, cnmetabolism_facade_.population, g)
except KeyError:
print(
'Function name error in step_callback keys. It should be nitrate_uptake')
# run CN-Metabolism
cnmetabolism_facade_.run(Tair, Tsoil)
# Adel 3D plant save
# if t_cnmetabolism % 24 == 0:
# adel_wheat.scene(g).save(os.path.join(OUTPUTS_DIRPATH, 'ADEL', 't{}.bgeom'.format(t_cnmetabolism)))
# append outputs at current step to global lists
if (stored_times == 'all') or (t in stored_times):
axes_outputs, elements_outputs, hiddenzones_outputs, organs_outputs, soils_outputs = build_outputs_.build_outputs_df_from_MTG()
all_simulation_steps.append(t)
axes_all_data_list.append(axes_outputs)
organs_all_data_list.append(organs_outputs)
hiddenzones_all_data_list.append(hiddenzones_outputs)
elements_all_data_list.append(elements_outputs)
soils_all_data_list.append(soils_outputs)
execution_time = int(time.time() - current_time_of_the_system)
print('\n' 'Simulation run in {}'.format(str(datetime.timedelta(seconds=execution_time))))
finally:
# convert list of outputs into dataframes
outputs_df_dict = {}
for outputs_df_list, outputs_filename, index_columns in ((axes_all_data_list, AXES_OUTPUTS_FILENAME, AXES_INDEX_COLUMNS),
(organs_all_data_list, ORGANS_OUTPUTS_FILENAME, ORGANS_INDEX_COLUMNS),
(hiddenzones_all_data_list, HIDDENZONES_OUTPUTS_FILENAME, HIDDENZONES_INDEX_COLUMNS),
(elements_all_data_list, ELEMENTS_OUTPUTS_FILENAME, ELEMENTS_INDEX_COLUMNS),
(soils_all_data_list, SOILS_OUTPUTS_FILENAME, SOILS_INDEX_COLUMNS)):
outputs_filepath = os.path.join(OUTPUTS_DIRPATH, outputs_filename)
outputs_df = pd.concat(outputs_df_list, keys=all_simulation_steps, sort=False)
outputs_df.reset_index(0, inplace=True)
outputs_df.rename({'level_0': 't'}, axis=1, inplace=True)
outputs_df = outputs_df.reindex(index_columns + outputs_df.columns.difference(index_columns).tolist(), axis=1, copy=False)
if run_from_outputs:
outputs_df = pd.concat([previous_outputs_dataframes[outputs_filename], outputs_df], sort=False)
outputs_df.fillna(value=np.nan, inplace=True) # Convert back None to NaN
save_df_to_csv(outputs_df, outputs_filepath, OUTPUTS_PRECISION)
outputs_file_basename = outputs_filename.split('.')[0]
outputs_df_dict[outputs_file_basename] = outputs_df.reset_index()
# ---------------------------------------------
# ----- POST-PROCESSING -------
# ---------------------------------------------
if run_postprocessing:
# Retrieve outputs dataframes from precedent simulation run
if not run_simu:
outputs_df_dict = {}
for outputs_filename in (AXES_OUTPUTS_FILENAME,
ORGANS_OUTPUTS_FILENAME,
HIDDENZONES_OUTPUTS_FILENAME,
ELEMENTS_OUTPUTS_FILENAME,
SOILS_OUTPUTS_FILENAME):
outputs_filepath = os.path.join(OUTPUTS_DIRPATH, outputs_filename)
outputs_df = pd.read_csv(outputs_filepath, dtype={'is_over': str, 'is_growing': str})
outputs_file_basename = outputs_filename.split('.')[0]
outputs_df_dict[outputs_file_basename] = outputs_df
# Assert states_filepaths were not opened during simulation run meaning that other filenames were saved
tmp_filename = 'ACTUAL_{}.csv'.format(outputs_file_basename)
tmp_path = os.path.join(OUTPUTS_DIRPATH, tmp_filename)
assert not os.path.isfile(tmp_path), \
"File {} was saved because {} was opened during simulation run. Rename it before running postprocessing".format(tmp_filename, outputs_file_basename)
time_grid = outputs_df_dict['axes_outputs'].t.unique()
delta_t = (time_grid[1] - time_grid[0]) * HOUR_TO_SECOND_CONVERSION_FACTOR
else:
delta_t = CNMETABOLISM_TIMESTEP * HOUR_TO_SECOND_CONVERSION_FACTOR
# run the postprocessing
postprocessing = cnmetabolism_facade.CNMetabolismFacade.postprocessing(axes_outputs_df=outputs_df_dict[AXES_OUTPUTS_FILENAME.split('.')[0]],
hiddenzone_outputs_df=outputs_df_dict[HIDDENZONES_OUTPUTS_FILENAME.split('.')[0]],
organs_outputs_df=outputs_df_dict[ORGANS_OUTPUTS_FILENAME.split('.')[0]],
elements_outputs_df=outputs_df_dict[ELEMENTS_OUTPUTS_FILENAME.split('.')[0]],
soils_outputs_df=outputs_df_dict[SOILS_OUTPUTS_FILENAME.split('.')[0]],
delta_t=delta_t)
if hydraulics:
turgor_postprocessing = hydraulics_facade.hydraulicsFacade.postprocessing(axes_outputs_df=outputs_df_dict[AXES_OUTPUTS_FILENAME.split('.')[0]],
hiddenzone_outputs_df=outputs_df_dict[HIDDENZONES_OUTPUTS_FILENAME.split('.')[0]],
elements_outputs_df=outputs_df_dict[ELEMENTS_OUTPUTS_FILENAME.split('.')[0]],
organs_outputs_df=outputs_df_dict[ORGANS_OUTPUTS_FILENAME.split('.')[0]],
soils_outputs_df=outputs_df_dict[SOILS_OUTPUTS_FILENAME.split('.')[0]],
delta_t=delta_t)
# Merge with cnmetabolism postprocessing
mapping_scales = [('axes', AXES_INDEX_COLUMNS),
('elements', ELEMENTS_INDEX_COLUMNS),
('hiddenzones', HIDDENZONES_INDEX_COLUMNS),
('organs', ORGANS_INDEX_COLUMNS),
('soils', SOILS_INDEX_COLUMNS)]
for scale, index_cols in mapping_scales:
df_cnmetabolism = postprocessing.get(scale)
df_turgor = turgor_postprocessing.get(scale)
turgor_exclusive_cols = index_cols + [col for col in df_turgor.columns if col not in df_cnmetabolism.columns]
df_turgor_filtered = df_turgor[turgor_exclusive_cols]
# Left merge
postprocessing[scale] = pd.merge(df_cnmetabolism, df_turgor_filtered, on=index_cols, how='left')
for postprocessing_file_basename, postprocessing_filename, index_columns in (('axes', AXES_POSTPROCESSING_FILENAME, AXES_INDEX_COLUMNS),
('hiddenzones', HIDDENZONES_POSTPROCESSING_FILENAME, HIDDENZONES_INDEX_COLUMNS),
('organs', ORGANS_POSTPROCESSING_FILENAME, ORGANS_INDEX_COLUMNS),
('elements', ELEMENTS_POSTPROCESSING_FILENAME, ELEMENTS_INDEX_COLUMNS),
('soils', SOILS_POSTPROCESSING_FILENAME, SOILS_INDEX_COLUMNS)):
postprocessing_filepath = os.path.join(POSTPROCESSING_DIRPATH, postprocessing_filename)
postprocessing_df = postprocessing[postprocessing_file_basename]
postprocessing_df.rename({'level_0': 't'}, axis=1, inplace=True)
postprocessing_df = postprocessing_df.reindex(index_columns + postprocessing_df.columns.difference(index_columns).tolist(), axis=1, copy=False)
postprocessing_df.to_csv(postprocessing_filepath, na_rep='NA', index=False, float_format='%.{}f'.format(OUTPUTS_PRECISION))
# ---------------------------------------------
# ----- GRAPHS -------
# ---------------------------------------------
if generate_graphs:
# Delete previous graphs
graphs = os.listdir(GRAPHS_DIRPATH)
for graph in graphs:
if graph.endswith(".PNG"):
os.remove(os.path.join(GRAPHS_DIRPATH, graph))
if not run_postprocessing:
postprocessing = {}
for postprocessing_filename in (AXES_POSTPROCESSING_FILENAME,
ORGANS_POSTPROCESSING_FILENAME,
HIDDENZONES_POSTPROCESSING_FILENAME,
ELEMENTS_POSTPROCESSING_FILENAME,
SOILS_POSTPROCESSING_FILENAME):
postprocessing_filepath = os.path.join(POSTPROCESSING_DIRPATH, postprocessing_filename)
postprocessing_df = pd.read_csv(postprocessing_filepath)
postprocessing_file_basename = postprocessing_filename.split('_')[0]
postprocessing[postprocessing_file_basename] = postprocessing_df
outputs_df_dict = {}
for outputs_filename in (AXES_OUTPUTS_FILENAME,
ORGANS_OUTPUTS_FILENAME,
HIDDENZONES_OUTPUTS_FILENAME,
ELEMENTS_OUTPUTS_FILENAME,
SOILS_OUTPUTS_FILENAME):
outputs_filepath = os.path.join(OUTPUTS_DIRPATH, outputs_filename)
outputs_df = pd.read_csv(outputs_filepath, dtype={'is_over': str, 'is_growing': str})
outputs_file_basename = outputs_filename.split('.')[0]
outputs_df_dict[outputs_file_basename] = outputs_df
# Assert states_filepaths were not opened during simulation run meaning that other filenames were saved
tmp_filename = 'ACTUAL_{}.csv'.format(outputs_file_basename)
tmp_path = os.path.join(OUTPUTS_DIRPATH, tmp_filename)
assert not os.path.isfile(tmp_path), \
"File {} was saved because {} was opened during simulation run. Rename it before running postprocessing".format(
tmp_filename, outputs_file_basename)
# --- Generate graphs from postprocessing files
plt.ioff()
cnmetabolism_facade.CNMetabolismFacade.graphs(axes_postprocessing_df=postprocessing['axes'],
hiddenzones_postprocessing_df=postprocessing['hiddenzones'],
organs_postprocessing_df=postprocessing['organs'],
elements_postprocessing_df=postprocessing['elements'],
soils_postprocessing_df=postprocessing['soils'],
meteo_data=meteo, graphs_dirpath=GRAPHS_DIRPATH)
if hydraulics:
hydraulics_facade.hydraulicsFacade.graphs(axes_postprocessing_df=postprocessing['axes'],
hiddenzones_postprocessing_df=postprocessing['hiddenzones'],
organs_postprocessing_df=postprocessing['organs'],
elements_postprocessing_df=postprocessing['elements'],
soils_postprocessing_df=postprocessing['soils'],
meteo_data=meteo, graphs_dirpath=GRAPHS_DIRPATH)
# --- Additional graphs
from openalea.cnwgrass.integration import tools as integration_tools
data_obs = pd.read_csv(os.path.join(INPUTS_DIRPATH, 'Ljutovac2002.csv'))
integration_tools.additional_graphs(outputs_df_dict['axes_outputs'], outputs_df_dict['hiddenzones_outputs'], outputs_df_dict['elements_outputs'],
postprocessing['axes'], postprocessing['hiddenzones'], postprocessing['elements'], postprocessing['organs'],
plant_density[1], morphogenesis_facade_._simulation.model.parameters.RERmax.items(), GRAPHS_DIRPATH, data_obs)