Beispiel #1
0
def test_soil_water_potential_drops_faster_for_small_soil_reservoirs_than_bigger_ones():
    for soil_class in hydraulic.def_param_soil().keys():
        psi_soil_small = hydraulic.soil_water_potential(psi_soil_init=0., water_withdrawal=1., soil_class=soil_class,
                                                        soil_total_volume=1., psi_min=-3.)
        psi_soil_big = hydraulic.soil_water_potential(psi_soil_init=0., water_withdrawal=1., soil_class=soil_class,
                                                      soil_total_volume=2., psi_min=-3.)
        assert psi_soil_small < psi_soil_big
Beispiel #2
0
def test_soil_water_potential_decreases_as_water_withdrawal_increases():
    for soil_class in hydraulic.def_param_soil().keys():
        psi_soil = [
            hydraulic.soil_water_potential(psi_soil_init=0.,
                                           water_withdrawal=w,
                                           soil_class=soil_class,
                                           soil_total_volume=1,
                                           psi_min=-3.)
            for w in arange(0, 3, 0.1)
        ]
        assert all(x >= y for x, y in zip(psi_soil, psi_soil[1:]))
Beispiel #3
0
def solve_interactions(g, meteo, psi_soil, t_soil, t_sky_eff, vid_collar,
                       vid_base, length_conv, time_conv, rhyzo_total_volume,
                       params, form_factors, simplified_form_factors):
    """Computes gas-exchange, energy and hydraulic structure of plant's shoot jointly.

    Args:
        g: MTG object
        meteo (DataFrame): forcing meteorological variables
        psi_soil (float): [MPa] soil (root zone) water potential
        t_soil (float): [degreeC] soil surface temperature
        t_sky_eff (float): [degreeC] effective sky temperature
        vid_collar (int): id of the collar node of the mtg
        vid_base (int): id of the basal node of the mtg
        length_conv (float): [-] conversion factor from the `unit_scene_length` to 1 m
        time_conv (float): [-] conversion factor from meteo data time step to seconds
        rhyzo_total_volume (float): [m3] volume of the soil occupied with roots
        params (params): [-] :class:`hydroshoot.params.Params()` object

    """
    unit_scene_length = params.simulation.unit_scene_length

    hydraulic_structure = params.simulation.hydraulic_structure
    negligible_shoot_resistance = params.simulation.negligible_shoot_resistance
    soil_water_deficit = params.simulation.soil_water_deficit
    energy_budget = params.simulation.energy_budget

    par_photo = params.exchange.par_photo
    par_photo_n = params.exchange.par_photo_N
    par_gs = params.exchange.par_gs
    rbt = params.exchange.rbt

    mass_conv = params.hydraulic.MassConv
    xylem_k_max = params.hydraulic.Kx_dict
    xylem_k_cavitation = params.hydraulic.par_K_vul
    psi_min = params.hydraulic.psi_min

    solo = params.energy.solo

    irradiance_type2 = params.irradiance.E_type2

    leaf_lbl_prefix = params.mtg_api.leaf_lbl_prefix
    collar_label = params.mtg_api.collar_label

    soil_class = params.soil.soil_class
    dist_roots, rad_roots = params.soil.roots

    temp_step = params.numerical_resolution.t_step
    psi_step = params.numerical_resolution.psi_step
    max_iter = params.numerical_resolution.max_iter
    psi_error_threshold = params.numerical_resolution.psi_error_threshold
    temp_error_threshold = params.numerical_resolution.t_error_crit

    modelx, psi_critx, slopex = [
        xylem_k_cavitation[ikey]
        for ikey in ('model', 'fifty_cent', 'sig_slope')
    ]

    if hydraulic_structure:
        assert (
            par_gs['model'] != 'vpd'
        ), "Stomatal conductance model should be linked to the hydraulic strucutre"
    else:
        par_gs['model'] = 'vpd'
        negligible_shoot_resistance = True

        print "par_gs: 'model' is forced to 'vpd'"
        print "negligible_shoot_resistance is forced to True."

    # Initialize all xylem potential values to soil water potential
    for vtx_id in traversal.pre_order2(g, vid_base):
        g.node(vtx_id).psi_head = psi_soil

    # Temperature loop +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    t_error_trace = []
    it_step = temp_step

    # Initialize leaf  temperature to air temperature
    g.properties()['Tlc'] = energy.leaf_temperature_as_air_temperature(
        g, meteo, leaf_lbl_prefix)

    for it in range(max_iter):
        t_prev = deepcopy(g.property('Tlc'))

        # Hydraulic loop +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        if hydraulic_structure:
            psi_error_trace = []
            ipsi_step = psi_step
            for ipsi in range(max_iter):
                psi_prev = deepcopy(g.property('psi_head'))

                # Compute gas-exchange fluxes. Leaf T and Psi are from prev calc loop
                exchange.gas_exchange_rates(g, par_photo, par_photo_n, par_gs,
                                            meteo, irradiance_type2,
                                            leaf_lbl_prefix, rbt)

                # Compute sap flow and hydraulic properties
                hydraulic.hydraulic_prop(g,
                                         mass_conv=mass_conv,
                                         length_conv=length_conv,
                                         a=xylem_k_max['a'],
                                         b=xylem_k_max['b'],
                                         min_kmax=xylem_k_max['min_kmax'])

                # Update soil water status
                psi_collar = hydraulic.soil_water_potential(
                    psi_soil,
                    g.node(vid_collar).Flux * time_conv, soil_class,
                    rhyzo_total_volume, psi_min)

                if soil_water_deficit:
                    psi_collar = max(-1.3, psi_collar)
                else:
                    psi_collar = max(-0.7, psi_collar)

                    for vid in g.Ancestors(vid_collar):
                        g.node(vid).psi_head = psi_collar

                # Compute xylem water potential
                n_iter_psi = hydraulic.xylem_water_potential(
                    g,
                    psi_soil=psi_collar,
                    model=modelx,
                    psi_min=psi_min,
                    psi_error_crit=psi_error_threshold,
                    max_iter=max_iter,
                    length_conv=length_conv,
                    fifty_cent=psi_critx,
                    sig_slope=slopex,
                    dist_roots=dist_roots,
                    rad_roots=rad_roots,
                    negligible_shoot_resistance=negligible_shoot_resistance,
                    start_vid=vid_collar,
                    stop_vid=None,
                    psi_step=psi_step)

                psi_new = g.property('psi_head')

                # Evaluate xylem conversion criterion
                psi_error_dict = {}
                for vtx_id in g.property('psi_head').keys():
                    psi_error_dict[vtx_id] = abs(psi_prev[vtx_id] -
                                                 psi_new[vtx_id])

                psi_error = max(psi_error_dict.values())
                psi_error_trace.append(psi_error)

                print 'psi_error = ', round(
                    psi_error, 3
                ), ':: Nb_iter = %d' % n_iter_psi, 'ipsi_step = %f' % ipsi_step

                # Manage temperature step to ensure convergence
                if psi_error < psi_error_threshold:
                    break
                else:
                    try:
                        if psi_error_trace[-1] >= psi_error_trace[
                                -2] - psi_error_threshold:
                            ipsi_step = max(0.05, ipsi_step / 2.)
                    except IndexError:
                        pass

                    psi_new_dict = {}
                    for vtx_id in psi_new.keys():
                        psix = psi_prev[vtx_id] + ipsi_step * (
                            psi_new[vtx_id] - psi_prev[vtx_id])
                        psi_new_dict[vtx_id] = psix

                    g.properties()['psi_head'] = psi_new_dict

        else:
            # Compute gas-exchange fluxes. Leaf T and Psi are from prev calc loop
            exchange.gas_exchange_rates(g, par_photo, par_photo_n, par_gs,
                                        meteo, irradiance_type2,
                                        leaf_lbl_prefix, rbt)

            # Compute sap flow and hydraulic properties
            hydraulic.hydraulic_prop(g,
                                     mass_conv=mass_conv,
                                     length_conv=length_conv,
                                     a=xylem_k_max['a'],
                                     b=xylem_k_max['b'],
                                     min_kmax=xylem_k_max['min_kmax'])

        # End Hydraulic loop +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        # Compute leaf temperature
        if energy_budget:
            leaves_length = energy.get_leaves_length(
                g,
                leaf_lbl_prefix=leaf_lbl_prefix,
                unit_scene_length=unit_scene_length)
            leaf_wind_speed = energy.leaf_wind_as_air_wind(
                g, meteo, leaf_lbl_prefix)
            gbH = energy.heat_boundary_layer_conductance(
                leaves_length, leaf_wind_speed)
            t_init = g.property('Tlc')
            ev = g.property('E')
            ei = g.property('Ei')
            g.properties()['Tlc'], t_iter = energy.leaf_temperature(
                g,
                meteo,
                t_soil,
                t_sky_eff,
                t_init=t_init,
                form_factors=form_factors,
                gbh=gbH,
                ev=ev,
                ei=ei,
                solo=solo,
                ff_type=simplified_form_factors,
                leaf_lbl_prefix=leaf_lbl_prefix,
                max_iter=max_iter,
                t_error_crit=temp_error_threshold,
                t_step=temp_step)

            # t_iter_list.append(t_iter)
            t_new = deepcopy(g.property('Tlc'))

            # Evaluation of leaf temperature conversion creterion
            error_dict = {
                vtx: abs(t_prev[vtx] - t_new[vtx])
                for vtx in g.property('Tlc').keys()
            }

            t_error = round(max(error_dict.values()), 3)
            print 't_error = ', t_error, 'counter =', it, 't_iter = ', t_iter, 'it_step = ', it_step
            t_error_trace.append(t_error)

            # Manage temperature step to ensure convergence
            if t_error < temp_error_threshold:
                break
            else:
                assert (it <= max_iter
                        ), 'The energy budget solution did not converge.'

                try:
                    if t_error_trace[
                            -1] >= t_error_trace[-2] - temp_error_threshold:
                        it_step = max(0.001, it_step / 2.)
                except IndexError:
                    pass

                t_new_dict = {}
                for vtx_id in t_new.keys():
                    tx = t_prev[vtx_id] + it_step * (t_new[vtx_id] -
                                                     t_prev[vtx_id])
                    t_new_dict[vtx_id] = tx

                g.properties()['Tlc'] = t_new_dict
Beispiel #4
0
def run(g, wd, scene=None, write_result=True, **kwargs):
    """
    Calculates leaf gas and energy exchange in addition to the hydraulic structure of an individual plant.

    :Parameters:
    - **g**: a multiscale tree graph object
    - **wd**: string, working directory
    - **scene**: PlantGl scene
    - **kwargs** can include:
        - **psi_soil**: [MPa] predawn soil water potential
        - **gdd_since_budbreak**: [°Cd] growing degree-day since bubreak
        - **sun2scene**: PlantGl scene, when prodivided, a sun object (sphere) is added to it
        - **soil_size**: [cm] length of squared mesh size
    """
    print('++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++')
    print('+ Project: ', wd)
    print('++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++')
    time_on = datetime.now()

    # Read user parameters
    params_path = wd + 'params.json'
    params = Params(params_path)

    output_index = params.simulation.output_index

    # ==============================================================================
    # Initialisation
    # ==============================================================================
    #   Climate data
    meteo_path = wd + params.simulation.meteo
    meteo_tab = read_csv(meteo_path, sep=';', decimal='.', header=0)
    meteo_tab.time = DatetimeIndex(meteo_tab.time)
    meteo_tab = meteo_tab.set_index(meteo_tab.time)

    #   Adding missing data
    if 'Ca' not in meteo_tab.columns:
        meteo_tab['Ca'] = [400.] * len(meteo_tab)  # ppm [CO2]
    if 'Pa' not in meteo_tab.columns:
        meteo_tab['Pa'] = [101.3] * len(meteo_tab)  # atmospheric pressure

    #   Determination of the simulation period
    sdate = datetime.strptime(params.simulation.sdate, "%Y-%m-%d %H:%M:%S")
    edate = datetime.strptime(params.simulation.edate, "%Y-%m-%d %H:%M:%S")
    datet = date_range(sdate, edate, freq='H')
    meteo = meteo_tab.loc[datet, :]
    time_conv = {'D': 86.4e3, 'H': 3600., 'T': 60., 'S': 1.}[datet.freqstr]

    # Reading available pre-dawn soil water potential data
    if 'psi_soil' in kwargs:
        psi_pd = DataFrame([kwargs['psi_soil']] * len(meteo.time),
                           index=meteo.time, columns=['psi'])
    else:
        assert (isfile(wd + 'psi_soil.input')), "The 'psi_soil.input' file is missing."
        psi_pd = read_csv(wd + 'psi_soil.input', sep=';', decimal='.').set_index('time')
        psi_pd.index = [datetime.strptime(s, "%Y-%m-%d") for s in psi_pd.index]

    # Unit length conversion (from scene unit to the standard [m]) unit)
    unit_scene_length = params.simulation.unit_scene_length
    length_conv = {'mm': 1.e-3, 'cm': 1.e-2, 'm': 1.}[unit_scene_length]

    # Determination of cumulative degree-days parameter
    t_base = params.phenology.t_base
    budbreak_date = datetime.strptime(params.phenology.emdate, "%Y-%m-%d %H:%M:%S")

    if 'gdd_since_budbreak' in kwargs:
        gdd_since_budbreak = kwargs['gdd_since_budbreak']
    elif min(meteo_tab.index) <= budbreak_date:
        tdays = date_range(budbreak_date, sdate, freq='D')
        tmeteo = meteo_tab.loc[tdays, :].Tac.to_frame()
        tmeteo = tmeteo.set_index(DatetimeIndex(tmeteo.index).normalize())
        df_min = tmeteo.groupby(tmeteo.index).aggregate(np.min).Tac
        df_max = tmeteo.groupby(tmeteo.index).aggregate(np.max).Tac
        # df_tt = merge(df_max, df_min, how='inner', left_index=True, right_index=True)
        # df_tt.columns = ('max', 'min')
        # df_tt['gdd'] = df_tt.apply(lambda x: 0.5 * (x['max'] + x['min']) - t_base)
        # gdd_since_budbreak = df_tt['gdd'].cumsum()[-1]
        df_tt = 0.5 * (df_min + df_max) - t_base
        gdd_since_budbreak = df_tt.cumsum()[-1]
    else:
        raise ValueError('Cumulative degree-days temperature is not provided.')

    print('GDD since budbreak = %d °Cd' % gdd_since_budbreak)

    # Determination of perennial structure arms (for grapevine)
    # arm_vid = {g.node(vid).label: g.node(vid).components()[0]._vid for vid in g.VtxList(Scale=2) if
    #            g.node(vid).label.startswith('arm')}

    # Soil reservoir dimensions (inter row, intra row, depth) [m]
    soil_dimensions = params.soil.soil_dimensions
    soil_total_volume = soil_dimensions[0] * soil_dimensions[1] * soil_dimensions[2]
    rhyzo_coeff = params.soil.rhyzo_coeff
    rhyzo_total_volume = rhyzo_coeff * np.pi * min(soil_dimensions[:2]) ** 2 / 4. * soil_dimensions[2]

    # Counter clockwise angle between the default X-axis direction (South) and
    # the real direction of X-axis.
    scene_rotation = params.irradiance.scene_rotation

    # Sky and cloud temperature [degreeC]
    t_sky = params.energy.t_sky
    t_cloud = params.energy.t_cloud

    # Topological location
    latitude = params.simulation.latitude
    longitude = params.simulation.longitude
    elevation = params.simulation.elevation
    geo_location = (latitude, longitude, elevation)

    # Pattern
    ymax, xmax = map(lambda dim: dim / length_conv, soil_dimensions[:2])
    pattern = ((-xmax / 2.0, -ymax / 2.0), (xmax / 2.0, ymax / 2.0))

    # Label prefix of the collar internode
    vtx_label = params.mtg_api.collar_label

    # Label prefix of the leaves
    leaf_lbl_prefix = params.mtg_api.leaf_lbl_prefix

    # Label prefices of stem elements
    stem_lbl_prefix = params.mtg_api.stem_lbl_prefix

    E_type = params.irradiance.E_type
    tzone = params.simulation.tzone
    turtle_sectors = params.irradiance.turtle_sectors
    icosphere_level = params.irradiance.icosphere_level
    turtle_format = params.irradiance.turtle_format

    energy_budget = params.simulation.energy_budget
    print('Energy_budget: %s' % energy_budget)

    # Optical properties
    opt_prop = params.irradiance.opt_prop

    print('Hydraulic structure: %s' % params.simulation.hydraulic_structure)

    psi_min = params.hydraulic.psi_min

    # Parameters of leaf Nitrogen content-related models
    Na_dict = params.exchange.Na_dict

    # Computation of the form factor matrix
    form_factors = None
    if energy_budget:
        print('Computing form factors...')
        form_factors = energy.form_factors_simplified(
            g, pattern=pattern, infinite=True, leaf_lbl_prefix=leaf_lbl_prefix, turtle_sectors=turtle_sectors,
            icosphere_level=icosphere_level, unit_scene_length=unit_scene_length)

    # Soil class
    soil_class = params.soil.soil_class
    print('Soil class: %s' % soil_class)

    # Rhyzosphere concentric radii determination
    rhyzo_radii = params.soil.rhyzo_radii
    rhyzo_number = len(rhyzo_radii)

    # Add rhyzosphere elements to mtg
    rhyzo_solution = params.soil.rhyzo_solution
    print('rhyzo_solution: %s' % rhyzo_solution)

    if rhyzo_solution:
        if not any(item.startswith('rhyzo') for item in g.property('label').values()):
            vid_collar = architecture.mtg_base(g, vtx_label=vtx_label)
            vid_base = architecture.add_soil_components(g, rhyzo_number, rhyzo_radii,
                                                        soil_dimensions, soil_class, vtx_label)
        else:
            vid_collar = g.node(g.root).vid_collar
            vid_base = g.node(g.root).vid_base

            radius_prev = 0.

            for ivid, vid in enumerate(g.Ancestors(vid_collar)[1:]):
                radius = rhyzo_radii[ivid]
                g.node(vid).Length = radius - radius_prev
                g.node(vid).depth = soil_dimensions[2] / length_conv  # [m]
                g.node(vid).TopDiameter = radius * 2.
                g.node(vid).BotDiameter = radius * 2.
                g.node(vid).soil_class = soil_class
                radius_prev = radius

    else:
        # Identifying and attaching the base node of a single MTG
        vid_collar = architecture.mtg_base(g, vtx_label=vtx_label)
        vid_base = vid_collar

    g.node(g.root).vid_base = vid_base
    g.node(g.root).vid_collar = vid_collar

    # Initializing sapflow to 0
    for vtx_id in traversal.pre_order2(g, vid_base):
        g.node(vtx_id).Flux = 0.

    # Addition of a soil element
    if 'Soil' not in g.properties()['label'].values():
        if 'soil_size' in kwargs:
            if kwargs['soil_size'] > 0.:
                architecture.add_soil(g, kwargs['soil_size'])
        else:
            architecture.add_soil(g, 500.)

    # Suppression of undesired geometry for light and energy calculations
    geom_prop = g.properties()['geometry']
    vidkeys = []
    for vid in g.properties()['geometry']:
        n = g.node(vid)
        if not n.label.startswith(('L', 'other', 'soil')):
            vidkeys.append(vid)
    [geom_prop.pop(x) for x in vidkeys]
    g.properties()['geometry'] = geom_prop

    # Attaching optical properties to MTG elements
    g = irradiance.optical_prop(g, leaf_lbl_prefix=leaf_lbl_prefix,
                                stem_lbl_prefix=stem_lbl_prefix, wave_band='SW',
                                opt_prop=opt_prop)

    # Estimation of Nitroen surface-based content according to Prieto et al. (2012)
    # Estimation of intercepted irradiance over past 10 days:
    if not 'Na' in g.property_names():
        print('Computing Nitrogen profile...')
        assert (sdate - min(
            meteo_tab.index)).days >= 10, 'Meteorological data do not cover 10 days prior to simulation date.'

        ppfd10_date = sdate + timedelta(days=-10)
        ppfd10t = date_range(ppfd10_date, sdate, freq='H')
        ppfd10_meteo = meteo_tab.loc[ppfd10t, :]
        caribu_source, RdRsH_ratio = irradiance.irradiance_distribution(ppfd10_meteo, geo_location, E_type,
                                                                        tzone, turtle_sectors, turtle_format,
                                                                        None, scene_rotation, None)

        # Compute irradiance interception and absorbtion
        g, caribu_scene = irradiance.hsCaribu(mtg=g,
                                              unit_scene_length=unit_scene_length,
                                              source=caribu_source, direct=False,
                                              infinite=True, nz=50, ds=0.5,
                                              pattern=pattern)

        g.properties()['Ei10'] = {vid: g.node(vid).Ei * time_conv / 10. / 1.e6 for vid in g.property('Ei').keys()}

        # Estimation of leaf surface-based nitrogen content:
        for vid in g.VtxList(Scale=3):
            if g.node(vid).label.startswith(leaf_lbl_prefix):
                g.node(vid).Na = exchange.leaf_Na(gdd_since_budbreak, g.node(vid).Ei10,
                                                  Na_dict['aN'],
                                                  Na_dict['bN'],
                                                  Na_dict['aM'],
                                                  Na_dict['bM'])

    # Define path to folder
    output_path = wd + 'output' + output_index + '/'

    # Save geometry in an external file
    # HSArc.mtg_save_geometry(scene, output_path)

    # ==============================================================================
    # Simulations
    # ==============================================================================

    sapflow = []
    # sapEast = []
    # sapWest = []
    an_ls = []
    rg_ls = []
    psi_stem = {}
    Tlc_dict = {}
    Ei_dict = {}
    an_dict = {}
    gs_dict = {}

    # The time loop +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    for date in meteo.time:
        print("=" * 72)
        print('Date', date, '\n')

        # Select of meteo data
        imeteo = meteo[meteo.time == date]

        # Add a date index to g
        g.date = datetime.strftime(date, "%Y%m%d%H%M%S")

        # Read soil water potntial at midnight
        if 'psi_soil' in kwargs:
            psi_soil = kwargs['psi_soil']
        else:
            if date.hour == 0:
                try:
                    psi_soil_init = psi_pd.loc[date, :][0]
                    psi_soil = psi_soil_init
                except KeyError:
                    pass
            # Estimate soil water potntial evolution due to transpiration
            else:
                psi_soil = hydraulic.soil_water_potential(psi_soil,
                                                          g.node(vid_collar).Flux * time_conv,
                                                          soil_class, soil_total_volume, psi_min)

        if 'sun2scene' not in kwargs or not kwargs['sun2scene']:
            sun2scene = None
        elif kwargs['sun2scene']:
            sun2scene = display.visu(g, def_elmnt_color_dict=True, scene=Scene())

        # Compute irradiance distribution over the scene
        caribu_source, RdRsH_ratio = irradiance.irradiance_distribution(imeteo, geo_location, E_type, tzone,
                                                                        turtle_sectors, turtle_format, sun2scene,
                                                                        scene_rotation, None)

        # Compute irradiance interception and absorbtion
        g, caribu_scene = irradiance.hsCaribu(mtg=g,
                                              unit_scene_length=unit_scene_length,
                                              source=caribu_source, direct=False,
                                              infinite=True, nz=50, ds=0.5,
                                              pattern=pattern)

        # g.properties()['Ei'] = {vid: 1.2 * g.node(vid).Ei for vid in g.property('Ei').keys()}

        # Trace intercepted irradiance on each time step
        rg_ls.append(sum([g.node(vid).Ei / (0.48 * 4.6) * surface(g.node(vid).geometry) * (length_conv ** 2) \
                          for vid in g.property('geometry') if g.node(vid).label.startswith('L')]))

        # Hack forcing of soil temperture (model of soil temperature under development)
        t_soil = energy.forced_soil_temperature(imeteo)

        # Climatic data for energy balance module
        # TODO: Change the t_sky_eff formula (cf. Gliah et al., 2011, Heat and Mass Transfer, DOI: 10.1007/s00231-011-0780-1)
        t_sky_eff = RdRsH_ratio * t_cloud + (1 - RdRsH_ratio) * t_sky

        solver.solve_interactions(g, imeteo, psi_soil, t_soil, t_sky_eff,
                                  vid_collar, vid_base, length_conv, time_conv,
                                  rhyzo_total_volume, params, form_factors)

        # Write mtg to an external file
        if scene is not None:
            architecture.mtg_save(g, scene, output_path)

        # Plot stuff..
        sapflow.append(g.node(vid_collar).Flux)
        # sapEast.append(g.node(arm_vid['arm1']).Flux)
        # sapWest.append(g.node(arm_vid['arm2']).Flux)

        an_ls.append(g.node(vid_collar).FluxC)

        psi_stem[date] = deepcopy(g.property('psi_head'))
        Tlc_dict[date] = deepcopy(g.property('Tlc'))
        Ei_dict[date] = deepcopy(g.property('Eabs'))
        an_dict[date] = deepcopy(g.property('An'))
        gs_dict[date] = deepcopy(g.property('gs'))

        print('---------------------------')
        print('psi_soil', round(psi_soil, 4))
        print('psi_collar', round(g.node(3).psi_head, 4))
        print('psi_leaf', round(np.median([g.node(vid).psi_head for vid in g.property('gs').keys()]), 4))
        print('')
        # print('Rdiff/Rglob ', RdRsH_ratio)
        # print('t_sky_eff ', t_sky_eff)
        print('gs', np.median(list(g.property('gs').values())))
        print('flux H2O', round(g.node(vid_collar).Flux * 1000. * time_conv, 4))
        print('flux C2O', round(g.node(vid_collar).FluxC, 4))
        print('Tleaf ', round(np.median([g.node(vid).Tlc for vid in g.property('gs').keys()]), 2),
              'Tair ', round(imeteo.Tac[0], 4))
        print('')
        print("=" * 72)

    # End time loop +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # Write output
    # Plant total transpiration
    sapflow = [flow * time_conv * 1000. for flow in sapflow]

    # sapEast, sapWest = [np.array(flow) * time_conv * 1000. for i, flow in enumerate((sapEast, sapWest))]

    # Median leaf temperature
    t_ls = [np.median(list(Tlc_dict[date].values())) for date in meteo.time]

    # Intercepted global radiation
    rg_ls = np.array(rg_ls) / (soil_dimensions[0] * soil_dimensions[1])

    results_dict = {
        'Rg': rg_ls,
        'An': an_ls,
        'E': sapflow,
        # 'sapEast': sapEast,
        # 'sapWest': sapWest,
        'Tleaf': t_ls
    }

    # Results DataFrame
    results_df = DataFrame(results_dict, index=meteo.time)

    # Write
    if write_result:
        results_df.to_csv(output_path + 'time_series.output',
                          sep=';', decimal='.')

    time_off = datetime.now()

    print("")
    print("beg time", time_on)
    print("end time", time_off)
    print("--- Total runtime: %d minute(s) ---" % int((time_off - time_on).seconds / 60.))

    return results_df