team-8/hydroshoot/test/test_exchange.py
francesco-bufalini 0ba7189bfc Commit last-minute
2025-08-02 13:46:28 +02:00

451 lines
21 KiB
Python

from datetime import datetime
from numpy import arange, linspace, testing
from pandas import Series
from hydroshoot import exchange, utilities, constants
def setup_default_photosynthesis_params():
"""
Generates a dictionary containing default **25 degrees** paramter values of the Farquhar's model for Vitis vinifera cv. Syrah.
:Parameters:
- **Vcm25**: Maximum RuBP-saturated rate of carboxylation [umol m-2 s-1]
- **Jm25**: Maximum of electron transport [umol m-2 s-1]
- **cRd**: the coefficient of mitochondrial respiration to `Vcm25`
- **TPU25**: the rate of triose phosphate transport [umol m-2 s-1]
- **Kc25**: Michaelis-Menten constant for the carboxylase [umol mol-1]
- **Ko25**: Michaelis-Menten constant for the oxygenase [mmol mol-1]
- **Tx25**: CO2 compensation point in the absence of mitochondrial respiration [umol mol-1]
- **c**: empirical parameter defining the temperature response curves of each of Kc, Ko, Vcm, Jm, TPU, Rd and Tx
- **deltaHa**: Activation energy of the Arrhenius functions [kJ molCO2-1]
- **ds**: float, enthalpy of activation [KJ mol-1]
- **dHd**: float, enthalpy of deactivation [KJ mol-1]
"""
res = dict(
Vcm25=89.0,
Jm25=143.0,
cRd=0.008,
TPU25=10.0,
alpha=0.24,
Kc25=404.9,
Ko25=278.4,
Tx25=42.75,
ds=0.635,
dHd=200,
RespT_Kc={'model': 'Ahrenius', 'c': 38.05, 'deltaHa': 79.43},
RespT_Ko={'model': 'Ahrenius', 'c': 20.30, 'deltaHa': 36.38},
RespT_Vcm={'model': 'Ahrenius', 'c': 26.35, 'deltaHa': 65.33},
RespT_Jm={'model': 'Ahrenius', 'c': 17.57, 'deltaHa': 43.54},
RespT_TPU={'model': 'Ahrenius', 'c': 21.46, 'deltaHa': 53.1},
RespT_Rd={'model': 'Ahrenius', 'c': 18.72, 'deltaHa': 46.39},
RespT_Tx={'model': 'Ahrenius', 'c': 19.02, 'deltaHa': 37.83}
)
res.update({'Rd': res['cRd'] * res['Vcm25']})
return res
def setup_default_stomatal_conductance_params():
return dict(model='misson', g0=0.019, m0=5.278, psi0=-0.1, D0=30., n=1.85)
def setup_leaf_local_weather():
res = Series({'time': datetime(2012, 8, 1, 11),
'Tac': 26.84,
'Tlc': 26.84,
'hs': 43.77,
'Rg': 300.28742,
'u': 2.17,
'Ca': 400,
'Pa': 101.3,
'PPFD': 663.035})
res['vpd'] = utilities.vapor_pressure_deficit(temp_air=res['Tac'], temp_leaf=res['Tlc'], rh=res['hs'])
return res
def test_leaf_na_is_as_expected():
expected_result = 2.007
obtained_result = exchange.leaf_Na(age_gdd=1000., ppfd_10=38.64, a_n=-0.0008, b_n=3.3, a_m=6.471, b_m=56.635)
testing.assert_almost_equal(obtained_result, expected_result, decimal=3)
def test_leaf_na_reduces_as_age_gdd_increases():
assert exchange.leaf_Na(
age_gdd=0., ppfd_10=38.64, a_n=-0.0008, b_n=3.3, a_m=6.471, b_m=56.635) > exchange.leaf_Na(
age_gdd=1000., ppfd_10=38.64, a_n=-0.0008, b_n=3.3, a_m=6.471, b_m=56.635)
def test_leaf_na_reduces_as_cumulative_ppfd_reduces():
assert exchange.leaf_Na(
age_gdd=1000., ppfd_10=0.0, a_n=-0.0008, b_n=3.3, a_m=6.471, b_m=56.635) < exchange.leaf_Na(
age_gdd=1000., ppfd_10=38.64, a_n=-0.0008, b_n=3.3, a_m=6.471, b_m=56.635)
def test_leaf_na_is_null_when_cumulative_ppfd_and_relationship_intercept_are_null():
assert exchange.leaf_Na(age_gdd=1000., ppfd_10=0.0, a_n=-0.0008, b_n=3.3, a_m=6.471, b_m=0.0) == 0.0
def test_leaf_na_is_null_when_age_gdd_and_relationship_intercept_are_null():
assert exchange.leaf_Na(age_gdd=0.0, ppfd_10=38.64, a_n=-0.0008, b_n=0.0, a_m=6.471, b_m=56.635) == 0.0
def test_leaf_na_is_greater_or_equal_to_zero():
assert exchange.leaf_Na(age_gdd=10000.0, ppfd_10=38.64, a_n=-0.0008, b_n=3.3, a_m=6.471, b_m=56.635) == 0.0
def test_arrhenius_1_increases_as_temperature_increases():
param_names = ['Tx', 'Kc', 'Ko']
for param_name in param_names:
prev_value = exchange.arrhenius_1(param_name, 0, setup_default_photosynthesis_params())
for leaf_temperature in range(1, 50):
actual_value = exchange.arrhenius_1(param_name, leaf_temperature,
setup_default_photosynthesis_params())
assert actual_value > prev_value
prev_value = actual_value
def test_arrhenius_2_is_maximum_for_vcmax_at_39_degrees_celsius():
value_at_low_temperature = exchange.arrhenius_2('Vcmax', 0.0, setup_default_photosynthesis_params())
value_at_optimal_temperature = exchange.arrhenius_2('Vcmax', 39.0, setup_default_photosynthesis_params())
value_at_high_temperature = exchange.arrhenius_2('Vcmax', 50.0, setup_default_photosynthesis_params())
assert all([value_at_optimal_temperature > val for val in (value_at_low_temperature, value_at_high_temperature)])
def test_arrhenius_2_is_maximum_for_jmax_at_37_degrees_celsius():
value_at_low_temperature = exchange.arrhenius_2('Jmax', 0.0, setup_default_photosynthesis_params())
value_at_optimal_temperature = exchange.arrhenius_2('Jmax', 37.0, setup_default_photosynthesis_params())
value_at_high_temperature = exchange.arrhenius_2('Jmax', 50.0, setup_default_photosynthesis_params())
assert all([value_at_optimal_temperature > val for val in (value_at_low_temperature, value_at_high_temperature)])
def test_dhd_sensibility_decreases_as_leaf_water_potential_decreases():
prev_value = exchange.dHd_sensibility(psi=0.0,
temp=25, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
for psi in arange(0, -3, -0.1):
act_value = exchange.dHd_sensibility(psi,
temp=25, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
assert act_value <= prev_value
prev_value = act_value
def test_dhd_sensibility_is_not_affected_by_leaf_temperature_increases_when_leaf_water_potential_is_optimal():
value_at_optimal_psi = exchange.dHd_sensibility(psi=0.0,
temp=25, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
for temperature in range(0, 50):
assert value_at_optimal_psi == exchange.dHd_sensibility(psi=0.0,
temp=temperature, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
def test_dhd_sensibility_is_affected_by_leaf_temperature_when_leaf_water_potential_is_below_its_minimum_threshold():
value_at_optimal_psi = exchange.dHd_sensibility(psi=0.0,
temp=25, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
for temperature in range(0, 50):
assert value_at_optimal_psi > exchange.dHd_sensibility(psi=-2.0,
temp=temperature, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
def test_dhd_sensibility_is_affected_by_leaf_temperature_only_within_restricted_bounds():
low_temperature_bound = 35
high_temperature_bound = 40
value_at_lowest_bound = exchange.dHd_sensibility(psi=-2.0,
temp=low_temperature_bound, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
value_at_highest_bound = exchange.dHd_sensibility(psi=-2.0,
temp=high_temperature_bound, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
for temperature in range(0, 50):
if temperature < low_temperature_bound:
assert value_at_lowest_bound == exchange.dHd_sensibility(psi=-2.0,
temp=temperature, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
elif temperature < high_temperature_bound:
assert value_at_lowest_bound >= exchange.dHd_sensibility(psi=-2.0,
temp=temperature, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
else:
assert value_at_highest_bound == exchange.dHd_sensibility(psi=-2.0,
temp=temperature, dhd_max=200.,
dhd_inhib_beg=195., dHd_inhib_max=190.,
psi_inhib_beg=-.75, psi_inhib_max=-2.,
temp_inhib_beg=35, temp_inhib_max=40)
def test_compute_an_2par_increases_only_electron_transport_as_ppfd_increases():
vcmax = []
j_frac = []
tpu_triple = []
rd = []
for ppfd in range(0, 2000, 10):
res = exchange.compute_an_2par(setup_default_photosynthesis_params(), ppfd, leaf_temp=25.0)
vcmax.append(res[0])
j_frac.append(res[2])
tpu_triple.append(res[4])
rd.append(res[6])
# vcmax is independent from ppfd
assert all(x == y for x, y in zip(vcmax, vcmax[1:]))
# j increases with ppfd
assert all(x < y for x, y in zip(j_frac, j_frac[1:]))
# tpu is independent from ppfd
assert all(x == y for x, y in zip(tpu_triple, tpu_triple[1:]))
# rd is independent from ppfd
assert all(x == y for x, y in zip(rd, rd[1:]))
def test_compute_an_2par_affects_all_photosynthetic_parameters_by_temperature():
vcmax = []
j_frac = []
tpu_triple = []
rd = []
for temperature in range(-10, 45):
res = exchange.compute_an_2par(setup_default_photosynthesis_params(), ppfd=1800.,
leaf_temp=temperature)
vcmax.append(res[0])
j_frac.append(res[2])
tpu_triple.append(res[4])
rd.append(res[6])
# vcmax is dependent on temperature
assert all(x != y for x, y in zip(vcmax, vcmax[1:]))
# j is dependent on temperature
assert all(x != y for x, y in zip(j_frac, j_frac[1:]))
# tpu is dependent on temperature
assert all(x != y for x, y in zip(tpu_triple, tpu_triple[1:]))
# rd is dependent on temperature
assert all(x != y for x, y in zip(rd, rd[1:]))
def test_fvpd_3_raises_error_for_unrecognized_model():
try:
exchange.fvpd_3('any_model', vpd=3., psi=-0., psi_crit=-0.37, m0=5.278, steepness_tuzet=1.85, d0_leuning=30.)
except ValueError as err:
assert err.args[0] == "The 'model' argument must be one of the following ('misson','tuzet', 'linear' or 'vpd')."
def test_fvpd_3_increases_stress_factor_as_leaf_water_potential_decreases():
for model in ('misson', 'tuzet', 'linear', 'vpd'):
reduction_factor = []
for psi in arange(0, -3, -0.1):
reduction_factor.append(
exchange.fvpd_3(model, vpd=3., psi=psi, psi_crit=-0.37, m0=5.278, steepness_tuzet=1.85, d0_leuning=30.))
assert all(x >= y for x, y in zip(reduction_factor, reduction_factor[1:]))
def test_mesophyll_conductance_changes_with_temperature():
gm = [exchange.mesophyll_conductance(t, gm25=0.1025, activation_energy=49600., deactivation_energy=437400.,
entropy=1400.) for t in arange(-10, 46)]
assert all(x != y for x, y in zip(gm, gm[1:]))
def test_mesophyll_conductance_is_positive():
gm = [exchange.mesophyll_conductance(t, gm25=0.1025, activation_energy=49600., deactivation_energy=437400.,
entropy=1400.) for t in arange(-10, 46)]
assert all(x >= 0 for x in gm)
def test_mesophyll_conductance_is_maximum_at_36_degrees_celsius():
gm_max = exchange.mesophyll_conductance(36, gm25=0.1025, activation_energy=49600., deactivation_energy=437400.,
entropy=1400.)
gm = [exchange.mesophyll_conductance(t, gm25=0.1025, activation_energy=49600., deactivation_energy=437400.,
entropy=1400.) for t in arange(-10, 46)]
assert all(x <= gm_max for x in gm)
def test_boundary_layer_conductance_decreases_as_leaf_length_increases():
gb = [exchange.boundary_layer_conductance(l, wind_speed=2.0, atm_pressure=101.3, air_temp=25.,
ideal_gas_cst=constants.ideal_gaz_cst) for l in arange(0.001, 0.25, 0.02)]
assert all(x >= y for x, y in zip(gb, gb[1:]))
def test_boundary_layer_conductance_increases_as_wind_speed_increases():
gb = [exchange.boundary_layer_conductance(leaf_length=0.1, wind_speed=w, atm_pressure=101.3, air_temp=25.,
ideal_gas_cst=constants.ideal_gaz_cst) for w in arange(0.0, 5.0, 0.5)]
assert all(x <= y for x, y in zip(gb, gb[1:]))
def test_boundary_layer_conductance_is_weakly_dependent_on_atmospheric_pressure():
gb = [exchange.boundary_layer_conductance(leaf_length=0.1, wind_speed=2., atm_pressure=p, air_temp=25.,
ideal_gas_cst=constants.ideal_gaz_cst) for p in arange(90.3, 102.3)]
assert [testing.assert_almost_equal(x, y, decimal=3) for x, y in zip(gb, gb[1:])]
def test_boundary_layer_conductance_increases_as_air_temperature_increases():
gb = [exchange.boundary_layer_conductance(leaf_length=0.1, wind_speed=2.0, atm_pressure=101.3, air_temp=t,
ideal_gas_cst=constants.ideal_gaz_cst) for t in range(-10, 46)]
assert all(x <= y for x, y in zip(gb, gb[1:]))
def test_an_gs_ci_reduces_gas_exchange_rates_as_leaf_water_potential_decreases(
leaf_local_weather=setup_leaf_local_weather()):
an, _, _, gs = zip(
*[exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=leaf_local_weather['vpd'],
leaf_temperature=leaf_local_weather['Tac'],
leaf_water_potential=psi,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
for psi in arange(0, -3, -0.1)])
assert all(x >= y for x, y in zip(an, an[1:]))
assert all(x >= y for x, y in zip(gs, gs[1:]))
def test_an_gs_ci_changes_gas_exchange_rates_as_leaf_temperature_changes(leaf_local_weather=setup_leaf_local_weather()):
an, _, _, gs = zip(
*[exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=leaf_local_weather['vpd'],
leaf_temperature=t,
leaf_water_potential=0,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
for t in range(-10, 46)])
assert all(x != y for x, y in zip(an, an[1:]))
assert all(x != y for x, y in zip(gs, gs[1:]))
def test_an_gs_ci_yields_maximum_net_photosynthesis_at_31_degrees_celsius(
leaf_local_weather=setup_leaf_local_weather()):
an_max, *_ = exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=leaf_local_weather['vpd'],
leaf_temperature=31,
leaf_water_potential=0,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
an, *_ = zip(
*[exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=leaf_local_weather['vpd'],
leaf_temperature=t,
leaf_water_potential=0,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
for t in range(-10, 46)])
assert all(x <= an_max for x in an)
def test_an_gs_ci_yields_maximum_stomatal_conductance_at_34_degrees_celsius(
leaf_local_weather=setup_leaf_local_weather()):
*_, gs_max = exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=leaf_local_weather['vpd'],
leaf_temperature=34,
leaf_water_potential=0,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
*_, gs = zip(
*[exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=leaf_local_weather['vpd'],
leaf_temperature=t,
leaf_water_potential=0,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
for t in range(-10, 46)])
assert all(x <= gs_max for x in gs)
def test_an_gs_ci_yields_more_severe_stress_when_temperature_and_water_stresses_are_combined_than_separated(
leaf_local_weather=setup_leaf_local_weather()):
an_t, *_, gs_t = zip(
*[exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=leaf_local_weather['vpd'],
leaf_temperature=t,
leaf_water_potential=0,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
for t in range(-10, 46)])
an_t_psi, *_, gs_t_psi = zip(
*[exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=leaf_local_weather['vpd'],
leaf_temperature=t,
leaf_water_potential=-2,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
for t in range(-10, 46)])
assert all(x <= x_t for x, x_t in zip(an_t_psi, an_t))
assert all(x <= x_t for x, x_t in zip(gs_t_psi, gs_t))
def test_transpiration_rate_incrases_as_vapor_pressure_deficit_increases(leaf_local_weather=setup_leaf_local_weather()):
atmospheric_pressure = leaf_local_weather['Pa']
air_temp = leaf_local_weather['Tac']
leaf_temp = air_temp
gb = exchange.boundary_layer_conductance(leaf_length=0.1, wind_speed=leaf_local_weather['u'],
atm_pressure=atmospheric_pressure, air_temp=air_temp,
ideal_gas_cst=constants.ideal_gaz_cst)
vpd = utilities.vapor_pressure_deficit(temp_air=air_temp, temp_leaf=leaf_temp, rh=leaf_local_weather['hs'])
*_, gs = exchange.an_gs_ci(
absorbed_ppfd=leaf_local_weather['PPFD'],
vapor_pressure_deficit=vpd,
leaf_temperature=leaf_temp,
leaf_water_potential=0,
photo_params=setup_default_photosynthesis_params(),
gs_params=setup_default_stomatal_conductance_params(),
rbt=2. / 3.,
ca=400.)
transpiration = [exchange.calc_transpiration_rate(vpd=vpd, gs=gs, gb=gb, atm_pressure=atmospheric_pressure)
for vpd in linspace(vpd, 0, 10)]
assert all(x >= y for x, y in zip(transpiration, transpiration[1:]))