import glob
import copy
import math
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from openalea.mtg.algo import axis
from scipy import optimize, constants
from pathlib import Path
from openalea.hydroroot.display import plot
from openalea.hydroroot.read_file import read_archi_data
from openalea.hydroroot.main import root_builder, hydroroot_flow
from openalea.hydroroot import radius, flux, conductance
from openalea.hydroroot.init_parameter import Parameters
from openalea.hydroroot.conductance import set_conductances, axial
from openalea.hydroroot.water_solute_transport import pressure_calculation, pressure_calculation_no_non_permeating_solutes, \
init_some_MTG_properties, osmotic_p_peg
[docs]
def water_solute_model(parameter, df_archi =None, df_law =None,
df_cnf = None, df_JvP = None, Data_to_Optim = None, Flag_verbose = False,
data_to_use = 'all', output = None, optim_method = 'COBYLA', Flag_debug = False,
Flag_radius = True, Flag_Constraint = True, dK_constraint = -3.0e-2, Flag_w_Lpr = False,
Flag_w_cnf = False):
"""
Perform direct simulations or parameters adjustment to fit data of Jv(P) and/or cut and flow experiments.
Water and solute transport. **Works with constant radial conductivity.**
:param parameter: Parameter - (see :func: Parameters)
:param df_archi: DataFrame (None) - DataFrame with the architecture data (see below structure description)
:param df_law: DataFrame list (None) - DataFrame with the length law data (see below structure description)
:param df_cnf: DataFrame (None) - cut and flow data to fit (see below structure description)
:param df_JvP: DataFrame (None) - Jv(P) data to fit (see below structure description)
:param Data_to_Optim: string list (None) - list of parameters to adjust, if None perform direct simulation, empty list is equivalent to ['K', 'k', 'Js', 'Ps']
:param Flag_verbose: boolean (False) - if True print intermediary results, optimization details, final simulation outputs, etc.
:param data_to_use: string ('all') - data to fit either 'JvP' (Jv(P)), 'cnf' (cut and flow), or 'all' both
:param output: string (None) - if not None output filename
:param optim_method: string ('COBYLA') - solver method used in scipy.optimize.minimize see docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html
:param Flag_debug: boolean (False) - if True print intermediary values of parameters adjustment
:param Flag_radius: boolean (True) - if True use diameter recorded in architecture file if present, otherwise use ref_radius
:param Flag_Constraint: boolean (True) - if True apply constraint on axial conductance 1st derivative (with COBYLA constraint may not be respected)
:param dK_constraint: float (-3.0e-2) - lower bound of the axial conductance 1st derivative if Flag_Constraint = True (with COBYLA constraint may not be respected)
:param Flag_w_Lpr: boolean (False) - set weight to 1.0 / len(list_DP) on the JvP objective function
:param Flag_w_cnf: boolean (False) - set weight to len(cut_n_flow_length) on the cut and flow objective function
:return:
- d: DataFrame with results
- g_cut: dictionary with MTG at each cut
**Data_to_Optim list**
- 'K': optimize axial conductance K
- 'k': optimize radial conductivity k
- 'Js': optimize pumping rate Js
- 'Ps': optimize permeability
- 'Klr': optimize axial conductance of laterals Klr if <> than PR
- 'klr': optimize radial conductivity of laterals klr if <> than PR
- 'sigma': optimize the reflection coefficient
['K', 'k', 'Js', 'Ps'] will optimize the four parameters in the list.
But, having too many parameters with this routine using local optimizer from scipy may not lead to any results.
**df_archi column names**
- distance_from_base_(mm), lateral_root_length_(mm), order
**df_law**
- list of 2 dataframe with the length law data: the first for the 1st order laterals on the primary root, the
2nd for the laterals on laterals whatever their order (2nd, 3rd, ...)
- column names: LR_length_mm , relative_distance_to_tip
**df_cnf column names**
- arch: sample name that must be contained in the 'input_file' of the yaml file
- dP_Mpa: column with the working cut and flow pressure (in relative to the base) if constant, may be empty see below
- J0, J1, ..., Jn: columns that start with 'J' containing the flux values, 1st the for the full root, then 1st cut, 2d cut, etc.
- lcut1, ...., lcutn: columns starting with 'lcut' containing the maximum length to the base after each cut, 1st cut, 2d cut, etc. (not the for full root)
- dP0, dP1,.., dPn: column starting with 'dP' containing the working pressure (in relative to the base) of each steps (if not constant): full root, 1st cut, 2d cut, etc.
**df_JvP column names**
- arch: sample name that must be contained in the 'input_file' of the yaml file
- J0, J1, ..., Jn: columns that start with 'J' containing the flux values of each pressure steps
- dP0, dP1,.., dPn: column starting with 'dP' containing the working pressure (in relative to the base) of each steps
**outputfile (csv)**
- column names: 'max_length', 'Jexp cnf (uL/s)', 'Jv cnf (uL/s)', 'surface (m2)', 'length (m)', 'dp', 'Jexp(P)', 'Jv(P)', 'Cbase', 'kpr', 'klr', 'Js', 'Ps', 'F cnf','F Lpr', 'x pr', 'K1st pr', 'x lr', 'K1st lr', 'K pr', 'K lr'
i.e.: max length from the cut to the base, J cnf exp, J cnf sim, root surface, total root length, pressure (Jv(P),
J exp Jv(P), J sim Jv(P), solute concentration at the base (Jv(P)), radial conductivity PR, radial conductivity LR,
pumping rate, permeability, objective fct cnf, objective fct Jv(P), x pr distance to tip for PR, K 1st guess for PR,
the same for laterals if different, then axial conductances for PR and LR.
**Remark**
- radial conductivity: single value or list of 2 values [kpr, klr]: 1st value for PR and 2nd for LR
- axial conductance: it is possible to add K for LR
"""
if Data_to_Optim is None:
Data_to_Optim = [] # direct simulation
elif len(Data_to_Optim) == 0:
Data_to_Optim = ['K', 'k', 'Js', 'Ps'] # if Data_to_Optim = []
Flag_Optim_K = ('K' in Data_to_Optim) # optimize axial conductance K
Flag_Optim_Klr = ('Klr' in Data_to_Optim) # optimize axial conductance of laterals Klr if <> than PR
Flag_Optim_klr = ('klr' in Data_to_Optim) # optimize radial conductivity of laterals klr if <> than PR
Flag_Optim_Js = ('Js' in Data_to_Optim) # optimize pumping rate Js
Flag_Optim_Ps = ('Ps' in Data_to_Optim) # optimize permeability
Flag_Optim_k = ('k' in Data_to_Optim) # optimize radial conductivity k
Flag_Optim_sigma = ('sigma' in Data_to_Optim)
if df_cnf is None:
# force on JvP data
data_to_use = 'JvP'
elif df_JvP is None:
# force on cut and flow data
data_to_use = 'cnf'
# if both are none the simulation will be done using psi_base en ext given parameter and run as one JvP data point
results = {}
g_cut = {}
tip_id = {}
S_g = []
cut_n_flow_length = []
Jexp = []
result_cv = []
def fun_constraint(x):
"""
Calculation of the constraint for the optimize.minimize solver
array of 1 column with non-negative constraints, i.e. every c[i] >= 0
:param x: array of the parameters to optimize
:return: numpy arrays
"""
n = len(axial_data[1])
c = np.ones(n - 1)
l = axial_data[0]
# inequality constraints >= 0 for the axial conductance: line below <=> (x[i+1] - x[i])/(l[i+1] - l[i]) >= dK_constraint
for i in range(n - 1):
c[i] = x[i + 1] - x[i] - dK_constraint * (l[i + 1] - l[i])
# bounds as inequality constraints needed by solver 'COBYLA' but redundant for 'SLSQP' with bounds
for i in range(n, len(x)):
c = np.append(c, (x[i] - bnds[i][0]))
c = np.append(c, (bnds[i][1] - x[i]))
return c
def fun_bound_cobyla(x):
"""
Calculation of the constraint for the optimize.minimize solver COBYLA
bounds expressed as non-negative constraint
array of 1 column with non-negative constraints, i.e. every c[i] >= 0
:param x: array of the parameters to optimize
:return: numpy arrays
"""
c = []
for i in range(len(x)):
c.append(x[i] - bnds[i][0])
c.append(bnds[i][1] - x[i])
return np.array(c)
def fun(x):
"""
Calculation of the objective function (Residual sum of squares) done on Jv(P) and CnF data
:param x: array of the parameters to optimize
:return: F (float), the objective function
"""
# Kx pr, Kx lr, Js, Ps,k lr and k pr may be optimized depends on the Flags_Optim_
_x = x * xini
if iKpr > 0:
axial_data[1] = list(
_x[:iKpr + 1]) # np array : _x[:n] means the n 1st elements index [0;n-1] but _x[n] means _x index n
if not Flag_Optim_Klr:
_axial_lr = None
else:
_axial_lr = axial_lr
_axial_lr[1] = _x[iKpr + 1:iKlr + 1]
if Flag_Optim_Js:
Js = _x[iJs]
else:
Js = J_s
if Flag_Optim_Ps:
Ps = _x[iPs]
else:
Ps = P_s
if Flag_Optim_klr:
klr = _x[iklr]
else:
klr = k[1]
if Flag_Optim_k:
kpr = _x[ikpr]
else:
kpr = k[0]
if Flag_Optim_sigma:
sigma = _x[isig]
else:
sigma = Sigma
# set new K and k in the MTG
g = set_K_and_k(g_cut, axial_data, kpr, axial_lr = _axial_lr, k_lr = klr, nr = Nb_of_roots,
nl = len(cut_n_flow_length))
# run simulation Jv(P) and CnF
JvP, F_JvP, C = Jv_P_calculation(g, sigma, Js, Ps)
JvCnf, F_JvCnF, C = Jv_cnf_calculation(g, sigma, Js, Ps)
F = F_JvP + F_JvCnF
# n = len(axial_data[1])
# c = np.ones(n - 1)
# l = axial_data[0]
# for i in range(n - 1):
# if _x[i+1] - _x[i] - dK_constraint * (l[i+1] - l[i]) < 0.0:
# F += 1.0
if Flag_debug: print('{:0.3e}'.format(F), ' '.join('{:0.3e}'.format(i) for i in _x))
result_cv.append([F, kpr, klr, Ps, Js] + axial_data[1])
return F
def fun_JvP_only(x):
"""
Calculation of the objective function (Residual sum of squares) done on Jv(P) data
:param x: array of the parameters to optimize
:return: F (float), the objective function
"""
# Kx pr, Kx lr, Js, Ps,k lr and k pr may be optimized depends on the Flags_Optim_
_x = x * xini
if iKpr > 0:
axial_data[1] = list(_x[:iKpr + 1]) # np array : _x[:n] means the n 1st elements index [0;n-1] but _x[n] means _x index n
if not Flag_Optim_Klr:
_axial_lr = None
else:
_axial_lr = axial_lr
_axial_lr[1] = _x[iKpr + 1:iKlr + 1]
if Flag_Optim_Js:
Js = _x[iJs]
else:
Js = J_s
if Flag_Optim_Ps:
Ps = _x[iPs]
else:
Ps = P_s
if Flag_Optim_klr:
klr = _x[iklr]
else:
klr = k[1]
if Flag_Optim_k:
kpr = _x[ikpr]
else:
kpr = k[0]
if Flag_Optim_sigma:
sigma = _x[isig]
else:
sigma = Sigma
# set new K and k in the MTG
g = set_K_and_k(g_cut, axial_data, kpr, axial_lr = _axial_lr, k_lr = klr, nr = Nb_of_roots,
nl = len(cut_n_flow_length))
# run simulation Jv(P)
JvP, F, C = Jv_P_calculation(g, sigma, Js, Ps)
if Flag_debug: print('{:0.3e}'.format(F), ' '.join('{:0.3e}'.format(i) for i in _x))
result_cv.append([F, kpr, klr, Ps, Js] + axial_data[1])
return F
def fun_cnf_only(x):
"""
Calculation of the objective function (Residual sum of squares) done on CnF data
:param x: array of the parameters to optimize
:return: F (float), the objective function
"""
# Kx pr, Kx lr, Js, Ps,k lr and k pr may be optimized depends on the Flags_Optim_
_x = x * xini
if iKpr > 0: axial_data[1] = list(
_x[:iKpr + 1]) # np array : _x[:n] means the n 1st elements index [0;n-1] but _x[n] means _x index n
if not Flag_Optim_Klr:
_axial_lr = None
else:
_axial_lr = axial_lr
_axial_lr[1] = _x[iKpr + 1:iKlr + 1]
if Flag_Optim_Js:
Js = _x[iJs]
else:
Js = J_s
if Flag_Optim_Ps:
Ps = _x[iPs]
else:
Ps = P_s
if Flag_Optim_klr:
klr = _x[iklr]
else:
klr = k[1]
if Flag_Optim_k:
kpr = _x[ikpr]
else:
kpr = k[0]
if Flag_Optim_sigma:
sigma = _x[isig]
else:
sigma = Sigma
# set new K and k in the MTG
g = set_K_and_k(g_cut, axial_data, kpr, axial_lr = _axial_lr, k_lr = klr, nr = Nb_of_roots,
nl = len(cut_n_flow_length))
# run simulation Jv(P)
JvCnf, F, C = Jv_cnf_calculation(g, sigma, Js, Ps)
if Flag_debug: print('{:0.3e}'.format(F), ' '.join('{:0.3e}'.format(i) for i in _x))
result_cv.append([F, kpr, klr, Ps, Js] + axial_data[1])
return F
def set_K_and_k(g, axial_pr, k_pr, axial_lr = None, k_lr = None, nr = 1, nl = 0):
"""
set the axial conductance and the radial conductivity of the uncut root and the different cut roots
The vertices of the cut roots from the entire root may have changed therefore the vertices where the cuts are made
must be set with the correct K and k see the code
:param g: (dict) - dictionnary of MTG corresponding to the entire root and the cuts
:param axial_pr: (list) - the axial conductance, list of 2 lists of floats
:param k_pr: (float) - the radial conductivity
:param axial_lr: (list) - if not None the axial conductance of the laterals, list of 2 lists of floats
:param k_lr: (float) - if not None the radial conductivity of the laterals
:param nr: (int) - number of root, because with seminals the measurements may have been done with several roots
:param nl: (int) - number of cuts
:return: g
"""
# if axial_lr is None: axial_lr = axial_pr
# if k_lr is None: k_lr = k_pr
for ig in range(nr):
g[0, ig] = set_conductances(g[0, ig], axial_pr = axial_pr, k0_pr = k_pr, axial_lr = axial_lr, k0_lr = k_lr)
# set the different cut roots
for ic in range(1, nl + 1):
for v in g[ic, ig].vertices_iter(g[0, ig].max_scale()):
vid = g[ic, ig].property('original_vid')[v]
g[ic, ig].property('K')[v] = g[0, ig].property('K')[vid]
g[ic, ig].property('k')[v] = g[0, ig].property('k')[vid]
g[ic, ig].property('K_exp')[v] = g[0, ig].property('K_exp')[
vid] # needed in pressure_calculation if Cpeg because calculation of K
g[ic, ig].property('k0')[v] = g[0, ig].property('k0')[vid]
# difference with the resistance network we do not set k = K the real boundary condition is used
# with the help of label 'cut' see pressure_calculation
return g
def Jv_P_calculation(g, sigma, Js, Ps):
"""
Perform the calculation of the data Jv(P), i.e. for different pressure difference list_DP
Most of the variables are global variables, only the variables that change at each calculation g (MTG) or
that are able to be optimized (sigma, Js, Ps) are passed in arguments
The change of K and k have been taken into account in function set_K_and_k
Return
JvP a dictionnary of outgoing sap flux at each pressure step and for each roots (for the case they are several seminals)
C a dictionnary of the concentration map at each pressure step and for each roots (for the case they are several seminals)
F the objective fonction
:param g: (dict) - dictionnary of MTG corresponding to the entire root and the cuts
:param sigma: (float) - the reflection coefficient
:param Js: (float) - the pumping rate
:param Ps: (float) - the permeability
:return: JvP (dict), C {dict}, F (float)
"""
JvP = {}
C = {}
F = 0.0
data = None
row = None
col = None
for idP in range(len(list_DP)):
Jv = 0.0
for ig in range(Nb_of_roots):
g[0, ig] = flux.flux(g[0, ig], psi_e = psi_base + list_DP[idP], psi_base = psi_base,
invert_model = True)
g[0, ig] = init_some_MTG_properties(g[0, ig], tau = Js, Cini = Cini, t = 1, Ps = Ps)
nb_v = g[0, ig].nb_vertices()
Fdx = 1.0
Fdx_old = 1.
Jv_old = 1.
# Newton-Raphson schemes: in pressure_calculation_no_non_permeating_solutes calculation of dx,
# array with dP and dC variation of the variables between two Newton step. Then the Newton scheme stops when
# Fdx > eps see below
while Fdx > eps:
g[0, ig], dx, data, row, col = pressure_calculation_no_non_permeating_solutes(g[0, ig],
sigma = sigma,
Ce = Ce,
Pe = parameter.exp[
'psi_e'],
Pbase = parameter.exp[
'psi_base'],
Cse = Cse,
dP = list_DP[idP],
C_base = None)
Fdx = math.sqrt(sum(dx ** 2.0)) / nb_v
JvP[idP, ig] = g[0, ig].property('J_out')[1]
if abs(JvP[idP, ig] - Jv_old) < 1.0e-4:
break
if abs(Fdx - Fdx_old) < eps:
break
Fdx_old = Fdx
Jv_old = JvP[idP, ig]
Jv += JvP[idP, ig]
C[idP, ig] = copy.deepcopy(g[0, ig].property('C'))
F += w_Lpr * (Jv - list_Jext[idP]) ** 2.0
return JvP, F, C
def Jv_cnf_calculation(g, sigma, Js, Ps):
"""
Perform the calculation of the data CnF, i.e. for different cut length cut_n_flow_length
Most of the variables are global variables, only the variables that change at each calculation g (MTG) or
that are able to be optimizes (sigma, Js, Ps) are passed in arguments
Return
JvCnf a dictionnary of outgoing sap flux at each cut step and for each roots (for the case they are several seminals)
C a dictionnary of the concentration map at each cut step and for each roots (for the case they are several seminals)
F the objective fonction
:param g: (dict) - dictionnary of MTG corresponding to the entire root and the cuts
:param sigma: (float) - the reflection coefficient
:param Js: (float) - the pumping rate
:param Ps: (float) - the permeability
:return: JvCnf (dict), C {dict}, F (float)
"""
ic = 0
JvCnf = {}
C = {}
F = 0.0
Jv = 0.0
data = row = col = None
for ig in range(Nb_of_roots):
g[ic, ig] = flux.flux(g[ic, ig], psi_e = psi_base + DP_cnf[ic], psi_base = psi_base, invert_model = True)
g[ic, ig] = init_some_MTG_properties(g[ic, ig], tau = Js, Cini = Cini, t = 1, Ps = Ps)
nb_v = g[ic, ig].nb_vertices()
Fdx = 1.0
Fdx_old = 1.
Jv_old = 1.
# Newton-Raphson schemes: in pressure_calculation_no_non_permeating_solutes calculation of dx,
# array with dP and dC variation of the variables between two Newton step. Then the Newton scheme stops when
# Fdx > eps see below
while Fdx > eps:
# use pressure_calculation_no_non_permeating_solutes because the root is uncut so no PEG enter the root
g[ic, ig], dx, data, row, col = pressure_calculation_no_non_permeating_solutes(g[ic, ig], sigma = sigma,
Ce = Ce,
Pe = parameter.exp[
'psi_e'],
Pbase = parameter.exp[
'psi_base'],
Cse = Cse,
dP = DP_cnf[ic])
Fdx = math.sqrt(sum(dx ** 2.0)) / nb_v
JvCnf[ic, ig] = g[ic, ig].property('J_out')[1]
if abs(JvCnf[ic, ig] - Jv_old) < 1.0e-4:
break
if abs(Fdx - Fdx_old) < eps:
break
Fdx_old = Fdx
Jv_old = JvCnf[ic, ig]
Jv += JvCnf[ic, ig]
C[ic, ig] = copy.deepcopy(g[ic, ig].property('C'))
F += w_cnf * (Jv - Jexp[ic]) ** 2.0
for ic in range(1, len(cut_n_flow_length) + 1):
Jv = 0.0
data = row = col = None
for ig in range(Nb_of_roots):
g[ic, ig] = flux.flux(g[ic, ig], psi_e = psi_base + DP_cnf[ic], psi_base = psi_base,
invert_model = True)
g[ic, ig] = init_some_MTG_properties(g[ic, ig], tau = Js, Cini = Cini, Cpeg_ini = Cpeg_ini, t = 1,
Ps = Ps)
nb_v = g[ic, ig].nb_vertices()
Fdx = 1.0
Fdx_old = 1.
Jv_old = 1.
# Newton-Raphson schemes: in pressure_calculation calculation of dx,
# array with dP, dC and dCpeg (if any) variation of the variables between two Newton step. Then the Newton scheme stops when
# Fdx > eps see below
while Fdx > eps:
g[ic, ig], dx, data, row, col = routine_calculation(g[ic, ig], sigma = sigma,
Ce = Ce, Pe = parameter.exp['psi_e'],
Pbase = parameter.exp['psi_base'],
Cse = Cse, dP = DP_cnf[ic])
Fdx = math.sqrt(sum(dx ** 2.0)) / nb_v
JvCnf[ic, ig] = g[ic, ig].property('J_out')[1]
# if Flag_debug: print local_j, Fdx, (Fdx - Fdx_old)
if abs(JvCnf[ic, ig] - Jv_old) < 1.0e-4:
break
if abs(Fdx - Fdx_old) < eps:
break
Fdx_old = Fdx
Jv_old = JvCnf[ic, ig]
Jv += JvCnf[ic, ig]
C[ic, ig] = copy.deepcopy(g[ic, ig].property('C'))
F += w_cnf * (Jv - Jexp[ic]) ** 2.0
return JvCnf, F, C
# architecture file to dataframe
index=''
# if (df_archi is None) and parameter.archi['read_architecture']:
# # architecture with filename in aqua team format
# archi_f = glob.glob(parameter.archi['input_dir'] + parameter.archi['input_file'][0])
# archi_f = archi_f[0]
# df_archi = read_archi_data(archi_f) if parameter.archi['read_architecture'] else None
# index = archi_f.replace(glob.glob(parameter.archi['input_dir'])[0], "")
if parameter.archi['read_architecture']:
# architecture with filename in aqua team format
# archi_f = glob.glob(parameter.archi['input_dir'] + parameter.archi['input_file'][0])
# archi_f = archi_f[0]
fname = str(Path(parameter.archi['input_dir']) / parameter.archi['input_file'][0]) # deals with path in windows
archi_f = glob.glob(fname) # deals with wildcards ? deprecated ???
archi_f = archi_f[0]
if df_archi is None: df_archi = read_archi_data(archi_f)
index = archi_f.replace(glob.glob(parameter.archi['input_dir'])[0], "")
if len(parameter.archi['input_file']) > 0:
index = parameter.archi['input_file'][0]
# length law data: override if necessary
if df_law is not None:
parameter.archi['length_data'] = df_law
# dataframe used to save and export results: cnf and Jv(P)
_col_names = ['max_length', 'Jexp cnf (uL/s)', 'Jv cnf (uL/s)', 'surface (m2)', 'length (m)']
results = {}
for key in _col_names:
results[key] = []
_col_names2 = ['dp', 'Jexp(P)', 'Jv(P)', 'Cbase']
results2 = {}
for key in _col_names2:
results2[key] = []
############################
# get value from yaml file
############################
delta = parameter.archi['branching_delay'][0]
nude_length = parameter.archi['nude_length'][0]
seed = parameter.archi['seed'][0]
axfold = parameter.output['axfold'][0]
radfold = parameter.output['radfold'][0]
# Conductancies: mananging the fact there are or not different values between the primary and laterals
# and the fact there are multiply by axfold and radfold
k = []
if type(parameter.hydro['k0']) != list:
k.append(parameter.hydro['k0'] * radfold)
k.append(None)
else:
if len(parameter.hydro['k0']) > 1:
k.append(parameter.hydro['k0'][0] * radfold)
k.append(parameter.hydro['k0'][1] * radfold)
else:
k.append(parameter.hydro['k0'][0] * radfold)
k.append(None)
exp_axial = parameter.hydro['axial_conductance_data']
axial_data = ([exp_axial[0], exp_axial[1]])
axial_data = list(axial(axial_data, axfold))
if len(exp_axial) == 4:
axial_lr = ([exp_axial[2], exp_axial[3]])
axial_lr = list(axial(axial_lr, axfold))
else:
axial_lr = None #copy.deepcopy(axial_data)
J_s = parameter.solute['J_s']
P_s = parameter.solute['P_s']
Cse = parameter.solute['Cse'] * 1e-9 # mol/m3 -> mol/microL, external permeating solute concentration
Ce = parameter.solute['Ce'] * 1e-9 # mol/m3 -> mol/microL, external non-permeating solute concentration
Cini = Cse # initialization solute concentration into the xylem vessels
Cpeg_ini = Ce # initialization non-permeating solute concentration into the xylem vessels: not 0.0 because more num instability
Sigma = parameter.solute['Sigma'] # reflection coefficient, fixed in this script
Pi_e_peg = osmotic_p_peg(Ce, unit_factor = 8.0e6) # from Ce mol/microL to g/g, external osmotic pressure of non-permeating in MPa
data = None
row = None
col = None
w_cnf = w_Lpr = 1. # weight on cnf cost function
# functions that resolve the matrix system used in the Newton-Raphson scheme
# different function depending on the presence of non-permeating solute, because there is one unknown less Cpeg
routine_calculation = None
if Ce <= 0.:
# no non-permeating solute present
routine_calculation = pressure_calculation_no_non_permeating_solutes
else:
routine_calculation = pressure_calculation
# the objective function calculation to call depending on the data we fit
if data_to_use == "cnf":
fun_objective = fun_cnf_only
elif data_to_use == "JvP":
fun_objective = fun_JvP_only
else:
fun_objective = fun
dK_constraint_max = 6.0e-2 # deprecated
_tol = 5.0e-7 # does not have significant impact !!?? used in some minimize.optimize solver
eps = 1.0e-9 # global: stop criterion for the Newton-Raphson loop in Jv_P_calculation and Jv_cnf_calculation
# Parameter bounds
Kbnds = (1.0e-10, np.inf) # axial conductance
kbnds = (0.01, np.inf) # radial conductivity
Jbnds = (1e-15, np.inf) # Js
Pbnds = (1e-15, np.inf) # Ps
psi_base = parameter.exp['psi_base']
# default value for the pressure difference between the external medium and the base
DP_cnf = []
DP_cnf.append(parameter.exp['psi_e'] - psi_base)
# variables used for the results output see end of script
K = {}
K['x pr'] = axial_data[0]
K['K1st pr'] = axial_data[1]
dK1st = pd.DataFrame(K, columns = ['x pr', 'K1st pr'])
K = {}
if axial_lr:
K['x lr'] = axial_lr[0]
K['K1st lr'] = axial_lr[1]
else:
K['x lr'] = axial_data[0]
K['K1st lr'] = axial_data[1]
dKlr1st = pd.DataFrame(K, columns = ['x lr', 'K1st lr'])
# # architecture file to dataframe
# df_archi = read_archi_data(archi_f) if parameter.archi['read_architecture'] else None
# index = archi_f.replace(glob.glob(parameter.archi['input_dir'])[0], "")
# read the data measurements from data base, cut-n-flow: flux, cut length and pressure difference
if df_cnf is not None:
for key in df_cnf['arch']:
if str(key).lower() in index.lower():
_list = df_cnf[df_cnf.arch == key].filter(regex = '^J').dropna(axis = 1).values.tolist()
Jexp = _list[0] # basal output flux
_list = df_cnf[df_cnf.arch == key].filter(regex = '^lcut').dropna(axis = 1).values.tolist()
cut_n_flow_length = _list[0] # cut lengthes
_list = df_cnf[df_cnf.arch == key].filter(regex = '^dP').dropna(axis = 1).values.tolist()
# the pressure difference is usually constant but sometimes, due to flow meter saturation, it may change
# in that case a list of values is given
if len(_list[0]) != 0:
DP_cnf = _list[0]
else:
DP_cnf = []
DP_cnf.append(parameter.exp['psi_e'] - psi_base) # for compatibility reason with first analysis on arabidopsis
if len(DP_cnf) < len(cut_n_flow_length) + 1: # if constant we create the list with the constant value
for i in range(1, len(cut_n_flow_length) + 1): DP_cnf.append(DP_cnf[0])
parameter.exp['psi_e'] = psi_base + DP_cnf[0]
# read the data measurements from data base Jv(P): flux, pressure
if df_JvP is not None:
for key in df_JvP['arch']:
if str(key).lower() in index.lower():
_list = df_JvP[df_JvP.arch == key].filter(regex = '^J').dropna(axis = 1).values.tolist()
list_Jext = _list[0] # basal output flux
_list = df_JvP[df_JvP.arch == key].filter(regex = '^dP').dropna(axis = 1).values.tolist()
list_DP = _list[0] # delta pressure
## below juste to get data above a minimum dP
# dlpr = pd.DataFrame(list(zip(list_DP, list_Jext)), columns = ['dP', 'Jv'])
# dlpr = dlpr.sort_values('dP')[dlpr['dP']>0.05]
# list_DP = list(dlpr['dP'])
# list_Jext = list(dlpr['Jv'])
else:
list_Jext = [parameter.exp['Jv']]
list_DP = [parameter.exp['psi_e'] - psi_base]
if Flag_w_Lpr: w_Lpr = 1.0 / len(list_DP)
if Flag_w_cnf: w_cnf = len(cut_n_flow_length)
# building the MTG
###################
Nb_of_roots = 2 if "-L" in index else 1 # sometimes thera are 2 roots for a given measurement with seminals
if df_archi is None:
primary_length = parameter.archi['primary_length'][0]
else:
primary_length = 0.
_length = 0
_surface = 0
for ig in range(Nb_of_roots):
if ig == 1:
f2 = archi_f.replace("-L", "-R")
df_archi = read_archi_data(f2) if parameter.archi['read_architecture'] else None
g_cut[0, ig], _p, _l, _s, _seed = root_builder(df = df_archi,
primary_length = parameter.archi['primary_length'][0],
seed = parameter.archi['seed'][0],
delta = parameter.archi['branching_delay'][0],
nude_length = parameter.archi['nude_length'][0],
segment_length = parameter.archi['segment_length'],
length_data = parameter.archi['length_data'],
branching_variability = parameter.archi['branching_variability'],
order_max = parameter.archi['order_max'],
order_decrease_factor = parameter.archi['order_decrease_factor'],
ref_radius = parameter.archi['ref_radius'],
Flag_radius = Flag_radius)
if _p > primary_length: primary_length = _p
_length += _l
_surface += _s
base = {}
for v in g_cut[0, ig]:
base[v] = next(axis(g_cut[0, ig], v))
g_cut[0, ig].properties()['axisbase'] = base
S_g.append(_s)
# case where the primary is shorter than laterals
max_length = primary_length
mylength = g_cut[0, ig].property('mylength')
if max(mylength.values()) > max_length: max_length = max(mylength.values())
# set conductance
g_cut[0, ig] = set_conductances(g_cut[0, ig], axial_pr = axial_data, k0_pr = k[0], axial_lr = axial_lr,
k0_lr = k[1])
# flux calculation without solute transport a way to initialize
g_cut[0, ig] = flux.flux(g_cut[0, ig], psi_e = psi_base + DP_cnf[0], psi_base = psi_base, invert_model = True)
# add properties specific to solute transport
g_cut[0, ig].add_property('C') # permeating solute concentration
g_cut[0, ig].add_property('Cpeg') # non-permeating solute concentration needed if cut-n-flow with them in the medium
g_cut[0, ig].add_property('theta') # see init_some_MTG_properties
g_cut[0, ig].add_property('J_s') # see init_some_MTG_properties, at a certain time I tried varying Js with C
g_cut[0, ig].add_property('P_s') # see init_some_MTG_properties, at a certain time I tried varying Js with C
g_cut[0, ig].add_property('original_vid') # the indices change between the full root and the cut root a way
# to retrieve the original index see set_K_and_k
g_cut[0, ig].add_property('mu') # the viscosity of the sap because could change from the water value when
# non-permeating solute enter the cut roots
# a simple record of the original vertex number in the full architecture
# do this because below when we cut we reindex because equations system is resolved in matrix form on the
# so the vertices need to have proper indices
# MTG
d = {vid: vid for vid in g_cut[0, ig].vertices(g_cut[0, ig].max_scale())}
g_cut[0, ig].properties()['original_vid'] = d
# ############ longitudinal CUTS ####################################
ic = 1
for cut_length in cut_n_flow_length:
# print(cut_length)
tip_id[ic, ig] = flux.segments_at_length(g_cut[0, ig], cut_length, dl = parameter.archi['segment_length'])
g_cut[ic, ig] = flux.cut(g_cut[0, ig], cut_length, parameter.archi['segment_length'])
for i in tip_id[ic, ig]:
v = g_cut[0, ig].parent(i)
g_cut[ic, ig].property('label')[v] = 'cut' # labelling the vertices at cut ends
# Below reindex because the system is resolved in matrix form on the MTG so the vertices need to have proper indices
g_cut[ic, ig].reindex()
i = 0
tip_id[ic, ig] = [] # reinitializing because the cut can be at ramification then one parent for 2 different cut vertices
for vid in g_cut[ic, ig].vertices_iter(g_cut[ic, ig].max_scale()):
if g_cut[ic, ig].label(vid) == 'cut':
tip_id[ic, ig].append(vid)
i += 1
g_cut[ic, ig], surface = radius.compute_surface(g_cut[ic, ig])
S_g.append(surface)
ic += 1
# Optimization
##############
# the parameter are normalized with their inital values to limit scale effect between them, not the best the best would
# be to write the equation in dimensionless form but historicaly hydroroot was not written this way
iKpr = iKlr = iJs = iPs = ikpr = iklr = 0 # indices used to select the correct parameters in the array x, see fun for instance
if Data_to_Optim:
# setting bounds and initial values
ix = -1
bnds = [] # list of tuple for bounds
xini_list = [] # list of initial values of parameters
if Flag_Optim_K:
for var in axial_data[1]:
xini_list.append(var)
ix += len(axial_data[1]) # be careful with axial_data and axial_lr the indices will be use as end of list interval selection => +1
iKpr = int(ix)
if Flag_Optim_Klr:
if axial_lr is None: axial_lr = copy.deepcopy(axial_data)
for var in axial_lr[1]:
xini_list.append(var)
ix += len(axial_lr[1])
iKlr = int(ix)
if Flag_Optim_K or Flag_Optim_Klr:
for i, val in enumerate(xini_list):
bnds.append(Kbnds)
if Flag_Optim_Js:
xini_list.append(J_s)
bnds.append(Jbnds)
ix += 1
iJs = int(ix)
if Flag_Optim_Ps:
xini_list.append(P_s)
bnds.append(Pbnds)
ix += 1
iPs = int(ix)
if Flag_Optim_k:
xini_list.append(k[0])
bnds.append(kbnds)
ix += 1
ikpr = int(ix)
if Flag_Optim_klr:
if k[1] is None: k[1] = copy.deepcopy(k[0])
xini_list.append(k[1])
bnds.append(kbnds)
ix += 1
iklr = int(ix)
if Flag_Optim_sigma:
xini_list.append(Sigma)
if Sigma > 0.0:
b = 1.0 / Sigma
else:
b = 1.0
bnds.append((0.0, b))
ix += 1
isig = int(ix)
xini = np.array(xini_list)
x = np.ones(len(xini)) # the array of parameter that will be optimized, equal unity because we optimize the
# the parameters normalized by their initial value
# array used for constraints see optimize.minimize doc
n = len(x)
n1 = len(axial_data[1])
# linear constraints lb <= A.dot(x) <= ub
A = np.zeros((n, n))
lb = np.full(n, -np.inf)
ub = np.full(n, np.inf)
l = copy.deepcopy(parameter.hydro['axial_conductance_data'][0])
if Flag_Optim_Klr:
l.append(0)
l.append(0)
if Flag_Optim_k: l.append(0)
if Flag_Optim_Klr: l.append(0)
if Flag_Optim_K and Flag_Constraint:
a = dK_constraint # constraint on the 1st derivative
for i in range(n1 - 1): # downward derivative
A[i, i] = -1.
A[i, i + 1] = 1.
lb[i] = a * (l[i + 1] - l[i])
# ub[i] = dK_constraint_max * (l[i + 1] - l[i])
ineq_cons = ({'type': 'ineq', 'fun': fun_constraint}) # !! works for K, k, Ps and Js optimized
else:
# for the COLBYLA solver bounds are not managed as other see fun_bound_cobyla
ineq_cons = {'type': 'ineq', 'fun': fun_bound_cobyla}
constraints = optimize.LinearConstraint(A, lb, ub) if Flag_Constraint else None
if optim_method == 'COBYLA':
res = optimize.minimize(fun_objective, x, method = optim_method, constraints = [ineq_cons])
elif optim_method == 'SLSQP':
res = optimize.minimize(fun_objective, x, bounds = bnds, method = optim_method, constraints = [ineq_cons],
options = {'ftol': 1.0e-9, 'eps': 1e-1})
else:
res = optimize.minimize(fun_objective, x, bounds = bnds, method = optim_method)
# res = optimize.minimize(fun_objective, x, bounds = bnds, method = 'trust-constr', options={'finite_diff_rel_step': 1e-1})
# res = optimize.minimize(fun_objective, x, method='TNC', bounds = bnds)
# res = optimize.minimize(fun_objective, x, bounds = bnds, options={'ftol': _tol, 'eps': 1e-1})
# res = optimize.minimize(fun_objective, x, bounds = bnds, method='nelder-mead', options={'fatol': 1.0e-9})
# optimization results to parameters
n = len(axial_data[1])
_x = res.x * xini
if Flag_Optim_K:
axial_data[1] = list(_x[:iKpr + 1])
if Flag_Optim_Klr:
axial_lr[1] = _x[iKpr + 1:iKlr + 1]
if Flag_Optim_Js: J_s = _x[iJs]
if Flag_Optim_Ps: P_s = _x[iPs]
if Flag_Optim_klr:
k[1] = _x[iklr]
# else:
# k[1] = None
if Flag_Optim_k:
k[0] = _x[ikpr]
if Flag_Optim_sigma:
Sigma = _x[isig]
if Flag_verbose: print(res.x)
# Direct simulation with the optimized values or the values from the yaml file if no optimization asked
g_cut = set_K_and_k(g_cut, axial_data, k[0], axial_lr = axial_lr, k_lr = k[1], nr = Nb_of_roots,
nl = len(cut_n_flow_length))
if data_to_use in ['all', 'JvP']:
JvP, F_JvP, C = Jv_P_calculation(g_cut, Sigma, J_s, P_s)
for idP in range(len(list_DP)):
Jv = 0.0
C_base = 0.0
for ig in range(Nb_of_roots):
# C_base here is in the middle of the 1st MTG element because the boundary condition chosen here is
# dC/dx = 0, so the concentration at the root boundary is the same.
# if there are several roots (as when the experiment is done with 2 seminals), since the MTG elements
# are equals the tital C_base is the average
C_base += C[idP, ig][1] / float(Nb_of_roots)
Jv += JvP[idP, ig]
results2['dp'].append(list_DP[idP])
results2['Jv(P)'].append(Jv)
results2['Jexp(P)'].append(list_Jext[idP])
results2['Cbase'].append(C_base * 1e9)
if data_to_use in ['all', 'cnf']:
JvCnf, F_cnf, C = Jv_cnf_calculation(g_cut, Sigma, J_s, P_s)
for ic in range(len(cut_n_flow_length) + 1):
_surface = 0.
_length = 0.
Jv = 0.0
C_base = 0.0
for ig in range(Nb_of_roots):
g_cut[ic, ig], _s = radius.compute_surface(g_cut[ic, ig])
_l = g_cut[ic, ig].nb_vertices(scale = 1) * parameter.archi['segment_length']
_surface += _s
_length += _l
Jv += JvCnf[ic, ig]
C_base += C[ic, ig][1] / float(Nb_of_roots) # C_base calculation see above JvP case
if ic > 0: max_length = cut_n_flow_length[ic - 1]
results['max_length'].append(max_length)
results['length (m)'].append(_length)
results['surface (m2)'].append(_surface)
results['Jv cnf (uL/s)'].append(Jv)
results['Jexp cnf (uL/s)'].append(Jexp[ic])
## just some parameter calculations for display
for ic in range(int(len(g_cut)/Nb_of_roots)): # FB 250328: added loop over cut
js_tot = 0
for ig in range(Nb_of_roots):
g_cut[ic, ig].add_property('DP')
g_cut[ic, ig].add_property('DC')
g_cut[ic, ig].add_property('jsurf')
g_cut[ic, ig].add_property('js')
g_cut[ic, ig].add_property('DPi')
DP = g_cut[ic, ig].property('DP')
DC = g_cut[ic, ig].property('DC')
DPi = g_cut[ic, ig].property('DPi')
jsurf = g_cut[ic, ig].property('jsurf')
psi_in = g_cut[ic, ig].property('psi_in')
js = g_cut[ic, ig].property('js')
C = g_cut[ic, ig].property('C')
length = g_cut[ic, ig].property('length')
_radius = g_cut[ic, ig].property('radius')
j = g_cut[ic, ig].property('j')
for v in g_cut[ic, ig].vertices_iter(scale = 1):
js[v] = _radius[v] * 2 * np.pi * length[v] * (J_s + P_s * (Cse-C[v]) * 1e9)
js_tot += js[v]
DC[v] = -(Cse - C[v])*1e9
DPi[v] = DC[v] * constants.R * 293 * 1e-6 # 1e-6 because in MPa
DP[v] = parameter.exp['psi_base'] + DP_cnf[0] - psi_in[v]
# psi_in[v] -= psi_base # just to put in relative pressure
jsurf[v] = j[v] / (_radius[v] * 2 * np.pi * length[v])
dr = pd.DataFrame()
dr2 = pd.DataFrame()
F = F2 = 0.0
if Flag_verbose:
pd.set_option('display.max_columns', None)
pd.set_option('display.expand_frame_repr', False)
if data_to_use in ['all', 'JvP']:
dr2 = pd.DataFrame(results2, columns = _col_names2)
# dr2.sort_values(['dp'], inplace=True)
j = np.array(dr2.loc[:, ['Jv(P)', 'Jexp(P)']])
F2 = w_Lpr * np.sum(np.diff(j) ** 2.0)
if Flag_verbose:
print('****** JvP ******')
print(dr2)
if data_to_use in ['all', 'cnf']:
dr = pd.DataFrame(results, columns = _col_names)
j = np.array(dr.loc[:, ['Jv cnf (uL/s)', 'Jexp cnf (uL/s)']])
F = w_cnf * np.sum(np.diff(j) ** 2.0)
if Flag_verbose:
print('****** cut-n-flow ******')
print(dr)
d = pd.concat([dr, dr2], axis = 1).fillna("")
X = {}
X['kpr'] = [k[0]]
if k[1] is None:
X['klr'] = [k[0]]
else:
X['klr'] = [k[1]]
X['Js'] = [J_s]
X['Ps'] = [P_s]
X['F cnf'] = [F]
X['F Lpr'] = [F2]
dX = pd.DataFrame(X, columns = ['kpr', 'klr', 'Js', 'Ps', 'F cnf', 'F Lpr'])
K = {}
# K['x pr'] = axial_data[0]
K['K pr'] = axial_data[1]
dK = pd.DataFrame(K, columns = ['K pr'])
Klr = {}
if axial_lr:
Klr['K lr'] = axial_lr[1]
else:
Klr['K lr'] = axial_data[1]
dKlr = pd.DataFrame(Klr, columns = ['K lr'])
d = pd.concat([d, dX, dK1st, dK, dKlr1st, dKlr], axis = 1).fillna("")
if output is not None: d.to_csv(output, index = False)
if Flag_verbose:
print('****** End ******')
print('objective functions: ', 'F cnf: {:0.2e}'.format(F), 'F JvP: {:0.2e}'.format(F2), 'F tot: {:0.2e}'.format(F+F2))
print(index, ',', 'k: {:0.2f}'.format(k[0]), ',', 'Js: {:0.2e}'.format(J_s), ',', 'Ps: {:0.2e}'.format(P_s), ', K: [',
', '.join('{:0.2e}'.format(i) for i in axial_data[1]), ']')
return d, g_cut
[docs]
def pure_hydraulic_model(parameter = Parameters(), df_archi = None, df_law =None, df_exp = None,
Data_to_Optim = None, output = None, Flag_verbose = False,
Flag_radius = False, Flag_Constraint = True, dK_constraint = -3.0e-2, dk_max = 0.1):
"""
Perform direct simulations or parameters adjustment to fit data of cut and flow experiment.
Water transport only, electrical network analogy
:param parameter: Parameter - (see :func: Parameters)
:param df_archi: DataFrame (None) - DataFrame with the architecture data (see below structure description)
:param df_law: DataFrame list (None) - DataFrame with the length law data (see below structure description)
:param df_exp: DataFrame (None) - data to fit
:param Data_to_Optim: string list (None) - list of parameters to adjust, if None perform direct simulation, ['K', 'k']
:param output: string (None) - if not None output filename
:param Flag_verbose: boolean (False) - if True print intermediary results
:param Flag_radius: boolean (False) - if True use diameter recorded in architecture file if present, otherwise use ref_radius
:param Flag_Constraint: boolean (True) - if True apply constraint on axial conductance 1st derivative
:param dK_constraint: float (-3.0e-2) - lower bound of the axial conductance 1st derivative if Flag_Constraint = True
:param dk_max: float (0.1) - the convergence criteria on
:return:
- df: DataFrame with results
- g_cut: dictionary with MTG at each cut
**df_archi column names**
- distance_from_base_(mm), lateral_root_length_(mm), order
**df_law**
- list of 2 dataframe with the length law data: the first for the 1st order laterals on the primary root, the
2nd for the laterals on laterals whatever their order (2nd, 3rd, ...)
- column names: LR_length_mm , relative_distance_to_tip
**The adjustment is performed as follows**
1. pre-optimization with the adjustment of axfold and radfold, K and k factor, if only k adjustment is asked then step 1 is not performed
2. loop of two successive adjustments: 1st K adjustment then k adjustment. The loop stop when change of k is below dk_max
**Data_to_Optim list of string**
- 'K': optimize axial conductance K only
- 'k': optimize radial conductivity k only
- [ ] empty list or ['K', 'k']: optimize K and k
**df_exp column names**
- arch: sample name that must be contained in the 'input_file' of the yaml file
- J0, ..., Jn: columns containing the flux values of the full root, 1st cut, 2d cut, etc.
- lcut1, ...., lcutn: columns containing the maximum length to the base after each cut, 1st cut, 2d cut, etc. (the primary length of the full root is calculated from the architecture)
**outputfile**
- column names: 'plant', 'cut length (m)', 'max_length', 'k (10-8 m/s/MPa)', 'length (m)', 'surface (m2)', 'Jv (uL/s)', 'Jexp (uL/s)'
- if 'K' in Data_to_Optim add the following: 'x', 'K 1st', 'K optimized' the initial and adjusted K(x)
**Remark**
The routine is designed to work with a single value (float) for parameter.hydro['k0'].
**example**
>>> parameter = Parameters()
>>> filename='parameters_fig-2-B.yml'
>>> parameter.read_file(filename)
>>> fn = 'data/arabido_cnf_data.csv'
>>> df_exp = pd.read_csv(fn, sep = ',', keep_default_na = True)
>>> df = pure_hydraulic_model(parameter,df_exp=df_exp, Flag_verbose=True, Data_to_Optim = ['k', 'K'])
"""
if Data_to_Optim is None:
Data_to_Optim = [] # direct simulation
elif len(Data_to_Optim) == 0:
Data_to_Optim = ['K', 'k'] # if Data_to_Optim = []
Flag_Optim_K = ('K' in Data_to_Optim) # optimize axial conductance K
Flag_Optim_k = ('k' in Data_to_Optim) # optimize radial conductivity k
g_cut = {}
tip_id = {}
cut_n_flow_length = []
_tol = 1.0e-9
def hydro_calculation(g, axfold = 1., radfold = 1., axial_data = None, k_radial = None, psi_base = 0.1, psi_e = 0.1):
if axial_data is None: axial_data = parameter.hydro['axial_conductance_data']
if k_radial is None: k_radial = parameter.hydro['k0']
# compute axial & radial
Kexp_axial_data = conductance.axial(axial_data, axfold)
k_radial_data = conductance.radial(k_radial, axial_data, radfold)
## Step function
# k_radial_data = conductance.radial_step(k_radial,3.0,x_step = 0.02, dx = parameter.archi['segment_length'], scale = radfold)
# compute local jv and psi, global Jv, Keq
g, Keq, Jv = hydroroot_flow(g, segment_length = parameter.archi['segment_length'],
k0 = k_radial,
Jv = _Jv[0],
psi_e = psi_e,
psi_base = psi_base,
axial_conductivity_data = Kexp_axial_data,
radial_conductivity_data = k_radial_data)
return g, Keq, Jv
def fun1(x):
"""
Simulation of the flux at the different cut lengths according to the new parameter value
Implementation 1: only axfold (Kx factor) and radfold (k radial factor) are changed
:param x: the array of adjusted parameters
:return: F the sum((Jv - Jv_exp) ** 2.0)
"""
axfold = x[0]
radfold = x[-1]
g_cut['tot'], Keq, Jv = hydro_calculation(g_cut['tot'], radfold = radfold, axfold = axfold, psi_base = psi_base,
psi_e = psi_base + DP_cnf[0])
F = (Jv - _Jv[0]) ** 2.0
count = 1
for cut_length in cut_n_flow_length:
# _g = g_cut[str(cut_length)].copy() # not necessary and time-consuming
for vid in g_cut[str(cut_length)].vertices_iter(g_cut['tot'].max_scale()):
g_cut[str(cut_length)].property('K')[vid] = g_cut['tot'].property('K')[vid]
g_cut[str(cut_length)].property('k')[vid] = g_cut['tot'].property('k')[vid]
for i in tip_id[str(cut_length)]:
v = g_cut['tot'].parent(i)
g_cut[str(cut_length)].property('k')[v] = g_cut[str(cut_length)].property('K')[v]
g_cut[str(cut_length)] = flux.flux(g_cut[str(cut_length)], Jv = _Jv[count], psi_e = psi_base + DP_cnf[count], psi_base = psi_base,
invert_model = True, cut_and_flow = True)
Jv = g_cut[str(cut_length)].property('J_out')[1]
F += (Jv - _Jv[count]) ** 2.0
count += 1
return F
def fun2(x):
"""
Simulation of the flux at the different cut lengths according to the new parameter value
Implementation 2: only axial_data is changed
:param x: the array of adjusted parameters
:return: F the sum((Jv - Jv_exp) ** 2.0)
"""
# k0 = parameter.hydro['k0']
axial_data = copy.deepcopy(parameter.hydro['axial_conductance_data'])
axial_data[1] = list(x)
g_cut['tot'], Keq, Jv = hydro_calculation(g_cut['tot'], k_radial = k0 ,axial_data = axial_data, psi_base = psi_base,
psi_e = psi_base + DP_cnf[0])
F = (Jv - _Jv[0])**2.0
count = 1
for cut_length in cut_n_flow_length:
# _g = g_cut[str(cut_length)].copy() # not necessary and time-consuming
for vid in g_cut[str(cut_length)].vertices_iter(g_cut['tot'].max_scale()):
g_cut[str(cut_length)].property('K')[vid] = g_cut['tot'].property('K')[vid]
g_cut[str(cut_length)].property('k')[vid] = g_cut['tot'].property('k')[vid]
for i in tip_id[str(cut_length)]:
v = g_cut['tot'].parent(i)
g_cut[str(cut_length)].property('k')[v] = g_cut[str(cut_length)].property('K')[v]
g_cut[str(cut_length)] = flux.flux(g_cut[str(cut_length)], Jv = _Jv[count], psi_e = psi_base + DP_cnf[count], psi_base = psi_base,
invert_model = True, cut_and_flow = True)
Jv = g_cut[str(cut_length)].property('J_out')[1]
F += (Jv - _Jv[count])**2.0
count += 1
return F
def fun3(x):
"""
Simulation of the flux at the different cut lengths according to the new parameter value
Implementation 3: only k is changed
:param x: the array of adjusted parameters
:return: F the sum((Jv - Jv_exp) ** 2.0)
"""
g_cut['tot'] = conductance.compute_k(g_cut['tot'] , k0 = x[0])
g_cut['tot'] = flux.flux(g_cut['tot'], Jv = _Jv[0], psi_e = psi_base + DP_cnf[0], psi_base = psi_base,
invert_model = True)
Jv = g_cut['tot'].property('J_out')[1]
F = (Jv - _Jv[0]) ** 2.0
count = 1
for cut_length in cut_n_flow_length:
# _g = g_cut[str(cut_length)].copy() # not necessary and time-consuming
for vid in g_cut[str(cut_length)].vertices_iter(g_cut['tot'].max_scale()):
g_cut[str(cut_length)].property('K')[vid] = g_cut['tot'].property('K')[vid]
g_cut[str(cut_length)].property('k')[vid] = g_cut['tot'].property('k')[vid]
for i in tip_id[str(cut_length)]:
v = g_cut['tot'].parent(i)
g_cut[str(cut_length)].property('k')[v] = g_cut[str(cut_length)].property('K')[v]
g_cut[str(cut_length)] = flux.flux(g_cut[str(cut_length)], Jv = _Jv[count], psi_e = psi_base + DP_cnf[count], psi_base = psi_base,
invert_model = True, cut_and_flow = True)
Jv = g_cut[str(cut_length)].property('J_out')[1]
F += (Jv - _Jv[count]) ** 2.0
count += 1
return F
# architecture with filename in aqua team format
if len(parameter.archi['input_file']) > 0:
if df_archi is None:
# archi_f = glob.glob(parameter.archi['input_dir'] + parameter.archi['input_file'][0])
# archi_f = archi_f[0]
fname = str(Path(parameter.archi['input_dir']) / parameter.archi['input_file'][0]) # deals with path in windows
archi_f = glob.glob(fname) # deals with wildcards ? deprecated ???
archi_f = archi_f[0]
df_archi = read_archi_data(archi_f) if parameter.archi['read_architecture'] else None
index = archi_f.replace(glob.glob(parameter.archi['input_dir'])[0],"")
else:
index = ''
# length law data: override if necessary
if df_law is not None:
parameter.archi['length_data'] = df_law
psi_e = parameter.exp['psi_e']
psi_base = parameter.exp['psi_base']
columns = ['plant', 'cut length (m)', 'max_length', 'k (10-9 m/s/MPa)', 'length (m)', 'surface (m2)',
'Jv (uL/s)', 'Jexp (uL/s)']
results = {}
for key in columns:
results[key] = []
# read the data measurements from data base
if df_exp is not None:
for key in df_exp['arch']:
if str(key).lower() in index.lower():
_list = df_exp[df_exp.arch == key].filter(regex = '^J').dropna(axis = 1).values.tolist()
# parameter.exp['Jv'] = _list[0][0] # basal output flux full root (uncut)
# _Jv = _list[0][1:] # basal output flux cut root
_Jv = _list[0]
_list = df_exp[df_exp.arch == key].filter(regex = '^lcut').dropna(axis = 1).values.tolist()
cut_n_flow_length = _list[0] # cut lengthes
_list = df_exp[df_exp.arch == key].filter(regex = '^dP').dropna(axis = 1).values.tolist()
# the pressure difference is usually constant but sometimes, due to flow meter saturation, it may change
# in that case a list of values is given
if len(_list[0]) != 0:
DP_cnf = _list[0]
else:
DP_cnf = []
DP_cnf.append(psi_e - psi_base) # for compatibility reason with first analysis on arabidopsis
if len(DP_cnf) < len(cut_n_flow_length)+1: # if constant we create the list with the constant value
for i in range(1, len(cut_n_flow_length) + 1): DP_cnf.append(DP_cnf[0])
else:
_Jv = [parameter.exp['Jv']]
cut_n_flow_length = []
DP_cnf = [psi_e - psi_base]
axfold = parameter.output['axfold'][0]
radfold = parameter.output['radfold'][0]
# g_cut['tot'], primary_length, _length, surface, seed = root_builder(df = df_archi, segment_length = parameter.archi['segment_length'],
# order_decrease_factor = parameter.archi['order_decrease_factor'], ref_radius = parameter.archi['ref_radius'])
g_cut['tot'], primary_length, _length, surface, seed = \
root_builder(df = df_archi,
primary_length = parameter.archi['primary_length'][0],
seed = parameter.archi['seed'][0],
delta = parameter.archi['branching_delay'][0],
nude_length = parameter.archi['nude_length'][0],
segment_length = parameter.archi['segment_length'],
length_data = parameter.archi['length_data'],
branching_variability = parameter.archi['branching_variability'],
order_max = parameter.archi['order_max'],
order_decrease_factor = parameter.archi['order_decrease_factor'],
ref_radius = parameter.archi['ref_radius'],
Flag_radius = Flag_radius)
g_cut['tot'], Keq, Jv = hydro_calculation(g_cut['tot'], psi_base = psi_base, psi_e = psi_base + DP_cnf[0])
###############################################################
#### WARNING : the mtg property 'position' must stay unchanged
#### because the axial conductivity is placed according to it
###############################################################
for cut_length in cut_n_flow_length:
tip_id[str(cut_length)] = \
flux.segments_at_length(g_cut['tot'], cut_length, dl = parameter.archi['segment_length'])
g_cut[str(cut_length)] = \
flux.cut_and_set_conductance(g_cut['tot'], cut_length, parameter.archi['segment_length'])
# g_cut[str(cut_length)], surface = radius.compute_surface(g_cut[str(cut_length)])
axial_data = list(conductance.axial(parameter.hydro['axial_conductance_data'], axfold))
###############################################################################################
## First adjustment: axfold, arfold that are coefficient factor of the radial conductivity k and
## and axial conductance K
###############################################################################################
if Flag_Optim_K:
# pre-optimization needed when there are several parameters so when K is optimized
if Flag_verbose: print("*********** pre_optimization ************************")
x = []
bnds = []
if Flag_Optim_K:
x.append(axfold)
bnds.append((1.0e-20, np.inf))
if Flag_Optim_k:
x.append(radfold)
bnds.append((1.0e-20, np.inf))
res = optimize.minimize(fun1, x, bounds = bnds, options = {'ftol': _tol})
if Flag_Optim_k:
radfold = res.x[-1] # always the last one even if the only one
if Flag_verbose: print('pre-optimization ar: {:0.2e}'.format(res.x[-1]))
if Flag_Optim_K:
axfold = res.x[0]
if Flag_verbose: print('pre-optimization ax: {:0.2e}'.format(res.x[0]))
if Flag_verbose: print("****************************************************************")
## update the conductivities according to the first adjustment
axial_data = list(conductance.axial(parameter.hydro['axial_conductance_data'], axfold))
k0 = parameter.hydro['k0'] *radfold
###############################################################################################
## 2d adjustment:
## -1 axial data adjusted
## -2 radial conductivit adjusted
## - 1 and 2 repeated until the k0 variation is below 0.1
###############################################################################################
x = []
x = axial_data[1]
bnds = []
n = len(x)
for i, val in enumerate(x):
bnds.append((1.0e-20, 1.0))
# linear constraints lb <= A.dot(x) <= ub
A = np.zeros((n, n))
lb = np.full(n, -np.inf)
ub = np.full(n, np.inf)
l = parameter.hydro['axial_conductance_data'][0]
a = dK_constraint # constraint on the 1st derivative
ni = n - 1
for i in range(ni): # downward derivative
A[i, i] = -1.
A[i, i + 1] = 1.
lb[i] = a * (l[i+1]-l[i])
i = ni
A[i, i-1] = -1.
A[i, i] = 1.
lb[i] = a * (l[i]-l[i-1])
k0_old = k0
F_old = (Jv - _Jv[0])**2.0
eps = 1e-9
F = 1.
if not (Flag_Optim_K or Flag_Optim_k):
k0_old2 = k0
else:
k0_old2 = k0 + 10
count2 = 0
while abs(k0-k0_old2) > dk_max:
k0_old2 = k0
# parameter.hydro['k0'] = k0
# #ajout Mistral
# _J_Sim=[]
# lcut = []
# lcut.append(0)
#
# g_cut['tot'] = conductance.compute_k(g_cut['tot'] , k0 = k0)
#
# g_cut['tot'] = flux.flux(g_cut['tot'], Jv = _Jv[0], psi_e = psi_base + DP_cnf[0], psi_base = psi_base,
# invert_model = True)
# _J_Sim.append(g_cut['tot'].property('J_out')[1])
#
# count = 1
# for cut_length in cut_n_flow_length:
# # _g = g_cut[str(cut_length)].copy() # not necessary and time-consuming
# lcut.append(g_cut['tot'].property('position')[1] - cut_length)
# for vid in g_cut[str(cut_length)].vertices_iter(g_cut['tot'].max_scale()):
# g_cut[str(cut_length)].property('K')[vid] = g_cut['tot'].property('K')[vid]
# g_cut[str(cut_length)].property('k')[vid] = g_cut['tot'].property('k')[vid]
#
# for i in tip_id[str(cut_length)]:
# v = g_cut['tot'].parent(i)
# g_cut[str(cut_length)].property('k')[v] = g_cut[str(cut_length)].property('K')[v]
#
# g_cut[str(cut_length)] = flux.flux(g_cut[str(cut_length)], Jv = _Jv[count], psi_e = psi_base + DP_cnf[count], psi_base = psi_base,
# invert_model = True, cut_and_flow = True)
# _J_Sim.append(g_cut[str(cut_length)].property('J_out')[1])
#
#
# count += 1
# plt.figure()
# plt.scatter(lcut,_Jv, c = 'black')
# plt.plot(lcut,_J_Sim, c = 'purple')
# plt.savefig(str(count2) + '.png')
# plt.close()
# count2 += 1
# # end ajout Mistral
## -1 axial data adjusted
#########################
constraints = optimize.LinearConstraint(A, lb, ub)
if Flag_Optim_K:
res = optimize.minimize(fun2, x, bounds = bnds, constraints = constraints, options={'ftol': _tol})
dKx = sum((x-res.x)**2.0)
axial_data[1] = list(res.x)
x = copy.deepcopy(res.x)
if Flag_verbose: print('finished minimize K: [', ', '.join('{:0.2e}'.format(i) for i in res.x), ']')
if Flag_Optim_k:
## -1 radial k adjusted
#######################
resk0 = optimize.minimize(fun3, k0, method = 'Nelder-Mead')
if Flag_verbose: print('finished minimize k0: , {:0.2e}'.format(resk0.x[0])) #,
# 'dk0**2.0 = {:0.2e}'.format((k0-resk0.x[0])**2.), 'dKx**2.0 = {:0.2e}'.format(dKx))
k0 = resk0.x[0]
else:
k0_old2 = k0
# print(resk0)
######################################
## Simulations with Kx and k adjusted
######################################
primary_length = g_cut['tot'].property('position')[1]
g_cut['tot'], Keq, Jv = hydro_calculation(g_cut['tot'], k_radial = k0 ,axial_data = axial_data, psi_base = psi_base,
psi_e = psi_base + DP_cnf[0])
# add some properties for display
g_cut['tot'].add_property('jsurf')
length = g_cut['tot'].property('length')
_radius = g_cut['tot'].property('radius')
j = g_cut['tot'].property('j')
jsurf = g_cut['tot'].property('jsurf')
for v in g_cut['tot'].vertices_iter(scale = 1):
jsurf[v] = j[v] / (_radius[v] * 2 * np.pi * length[v])
results['plant'].append(index)
results['max_length'].append(primary_length)
results['cut length (m)'].append(0.0)
results['k (10-9 m/s/MPa)'].append(k0)
results['length (m)'].append(_length)
results['surface (m2)'].append(surface)
results['Jv (uL/s)'].append(Jv)
results['Jexp (uL/s)'].append(_Jv[0])
count = 1
for cut_length in cut_n_flow_length:
g_cut[str(cut_length)] = g_cut[str(cut_length)].copy()
for vid in g_cut[str(cut_length)].vertices_iter(g_cut['tot'].max_scale()):
g_cut[str(cut_length)].property('K')[vid] = g_cut['tot'].property('K')[vid]
g_cut[str(cut_length)].property('k')[vid] = g_cut['tot'].property('k')[vid]
for i in tip_id[str(cut_length)]:
v = g_cut['tot'].parent(i)
g_cut[str(cut_length)].property('k')[v] = g_cut[str(cut_length)].property('K')[v]
g_cut[str(cut_length)] = flux.flux(g_cut[str(cut_length)], psi_e = psi_base + DP_cnf[count], psi_base = psi_base, invert_model = True)
Jv = g_cut[str(cut_length)].property('J_out')[1]
g_cut[str(cut_length)], surface = radius.compute_surface(g_cut[str(cut_length)])
_length = g_cut[str(cut_length)].nb_vertices(scale = 1) * parameter.archi['segment_length']
# add some properties for display
g_cut[str(cut_length)].add_property('jsurf')
length = g_cut[str(cut_length)].property('length')
_radius = g_cut[str(cut_length)].property('radius')
j = g_cut[str(cut_length)].property('j')
jsurf = g_cut[str(cut_length)].property('jsurf')
for v in g_cut[str(cut_length)].vertices_iter(scale = 1):
jsurf[v] = j[v] / (_radius[v] * 2 * np.pi * length[v])
primary_length = cut_length
results['plant'].append(index)
results['max_length'].append(primary_length)
results['cut length (m)'].append(g_cut['tot'].property('position')[1] - primary_length)
results['k (10-9 m/s/MPa)'].append(k0)
results['length (m)'].append(_length)
results['surface (m2)'].append(surface)
results['Jv (uL/s)'].append(Jv)
results['Jexp (uL/s)'].append(_Jv[count])
count += 1
dresults = pd.DataFrame(results, columns = columns)
optim_results = {}
optim_results['x'] = copy.deepcopy(parameter.hydro['axial_conductance_data'][0])
optim_results['K 1st'] = copy.deepcopy(parameter.hydro['axial_conductance_data'][1])
if Flag_Optim_K:
_x = list(res.x)
optim_results['K optimized'] = copy.deepcopy(_x)
else:
optim_results['K optimized'] = optim_results['K 1st']
doptim = pd.DataFrame(optim_results, columns = ['x', 'K 1st', 'K optimized'])
df = pd.concat([dresults, doptim], axis = 1)
if Flag_verbose:
pd.set_option('display.max_columns', None)
pd.set_option('display.expand_frame_repr', False)
print(dresults)
if Flag_Optim_K: print(doptim)
if output is not None: df.to_csv(output, index = False)
g_cut[0] = g_cut.pop('tot')
icut = 1
for cut_length in cut_n_flow_length:
g_cut[icut] = g_cut.pop(str(cut_length))
icut += 1
return df, g_cut