From 56494e2d4b6aeac6368f55b8c1b978546a83b03e Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Tue, 24 Mar 2026 09:02:33 +0100 Subject: [PATCH 01/41] implementation of simplified lookup table completed --- src/functional/Perirhizal.py | 295 +++++++++++++++++++++++++++++++++-- 1 file changed, 283 insertions(+), 12 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 405c96c40..92553c2d9 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -33,6 +33,8 @@ def __init__(self, ms=None): self.lookup_table = None # optional 4d look up table to find soil root interface potentials self.sp = None # corresponding van gencuchten soil parameter + + self.alpha_0 = 0.04 # a constant that is used for numerically solving the perirhizal waterflow def set_soil(self, sp): """sets VG parameters, and no look up table (slow)""" @@ -41,13 +43,31 @@ def set_soil(self, sp): self.lookup_table = None def open_lookup(self, filename): - """opens a look-up table from a file to quickly find soil root interface potentials""" + """ opens a look-up table from a file to quickly find soil root interface potentials """ npzfile = np.load(filename + ".npz") interface = npzfile["interface"] rx_, sx_, akr_, rho_ = npzfile["rx_"], npzfile["sx_"], npzfile["akr_"], npzfile["rho_"] soil = npzfile["soil"] self.lookup_table = RegularGridInterpolator((rx_, sx_, akr_, rho_), interface) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) + + def open_simp_lookup(self, filename): + """opens a smaller look-up table from a file to quickly find soil root interface potentials""" + npzfile = np.load(filename + "_simp.npz") + interface = npzfile["interface"] + inner_kr_b, base_mfp = npzfile["inner_kr_b_"], npzfile["base_mfp_"] + soil = npzfile["soil"] + self.lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp), interface) # method = "nearest" fill_value = None , bounds_error=False + self.sp = vg.Parameters(soil) + + def open_global_lookup(self, filename = "lookup_perirhizal_waterflow_global"): + """opens a global look-up table from a file to quickly find soil root interface potentials, this lookup table works for all van Genuchten parameter sets""" + npzfile = np.load(filename + ".npz") + interface, interface_vg = npzfile["interface"], npzfile["interface_vg"] + vg_m_, inner_kr_b_, base_mfp_, sx_ = npzfile["vg_m_"], npzfile["inner_kr_b_"], npzfile["base_mfp_"], npzfile["sx_"] + self.global_lookup_table = RegularGridInterpolator((vg_m_, inner_kr_b_, base_mfp_), interface) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_global_mfp = RegularGridInterpolator((vg_m_, sx_), interface_vg) + #.sp = vg.Parameters(soil) def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): """ @@ -62,6 +82,8 @@ def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): assert len(rx) == len(sx) == len(inner_kr) == len(rho), "rx, sx, inner_kr, and rho must have the same length" if self.lookup_table: rsx = self.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) + else if self.global_lookup_table: + rsx = self.soil_root_interface_potentials_global_table(rx, sx, inner_kr, rho) else: rsx = np.array([PerirhizalPython.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], self.sp) for i in range(0, len(rx))]) return rsx @@ -87,6 +109,51 @@ def soil_root_interface_(rx, sx, inner_kr, rho, sp): return sx rsx = root_scalar(fun, method="brentq", bracket=[min(rx, sx), max(rx, sx)]) return rsx.root + + @staticmethod + def soil_root_interface_simp_(inner_kr_b, base_mfp, sp): + """ + finds matric potential at the soil root interface for as single segment + + rx xylem matric potential [cm] + sx bulk soil matric potential [cm] + inner_kr root radius times hydraulic conductivity [cm/day] + rho geometry factor [1] (outer_radius / inner_radius) + sp soil parameter: van Genuchten parameter set (type vg.Parameters) + + inner_kr_b: inner_kr/b + base_mfp: -(inner_kr/b*rx+vg.fast_mfp[sp](sx)) + """ + fun = lambda x: vg.fast_mfp[sp](x) + inner_kr_b * x +base_mfp + x_int = [-16000.0,-1.0] # interval for search + rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) + return rsx.root + + @staticmethod + def soil_root_interface_global_(inner_kr_b, base_mfp, vg_m): + """ + finds matric potential at the soil root interface for as single segment + + rx xylem matric potential [cm] + sx bulk soil matric potential [cm] + inner_kr root radius times hydraulic conductivity [cm/day] + rho geometry factor [1] (outer_radius / inner_radius) + sp soil parameter: van Genuchten parameter set (type vg.Parameters) + + inner_kr_b: (inner_kr * alpha)/(alpha_0 * vg_Ks * b) + base_mfp: -((inner_kr * alpha)/(alpha_0 * Ks * b) * rx + vg.fast_mfp[sp_base](sx)) + vg_m: van genuchten parameter m + + (sp_base is the van genuchten parameter set of Ks = 1 and alpha = alpha_0, m is an input) + """ + + + + fun = lambda x: self.lookup_global_mfp(vg_m, x) + inner_kr_b * x +base_mfp + x_int = [-16000.0,-1.0] # interval for search + rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) + return rsx.root + def perirhizal_conductance_per_layer(self, h_bs, h_sr, sp): """ @@ -196,26 +263,26 @@ def create_lookup_mpi(self, filename, sp): self.sp = sp def create_lookup(self, filename, sp): - """ - Precomputes all soil root interface potentials for a specific soil type + """ + Precomputes all soil root interface potentials for a specific soil type and saves results into a 4D lookup table - + filename three files are written (filename, filename_, and filename_soil) - sp van genuchten soil parameters, , call - vg.create_mfp_lookup(sp) before + sp van genuchten soil parameters, , call + vg.create_mfp_lookup(sp) before """ rxn = 150 - rx_ = -np.logspace(np.log10(1.0), np.log10(16000), rxn) + rx_ = -np.logspace(np.log10(1.), np.log10(16000), rxn) rx_ = rx_ + np.ones((rxn,)) rx_ = rx_[::-1] sxn = 150 - sx_ = -np.logspace(np.log10(1.0), np.log10(16000), sxn) + sx_ = -np.logspace(np.log10(1.), np.log10(16000), sxn) sx_ = sx_ + np.ones((sxn,)) sx_ = sx_[::-1] akrn = 100 - akr_ = np.logspace(np.log10(1.0e-7), np.log10(1.0e-4), akrn) + akr_ = np.logspace(np.log10(1.e-7), np.log10(1.e-4), akrn) rhon = 30 - rho_ = np.logspace(np.log10(1.0), np.log10(200.0), rhon) + rho_ = np.logspace(np.log10(1.), np.log10(200.), rhon) interface = np.zeros((rxn, sxn, akrn, rhon)) for i, rx in enumerate(rx_): print(i) @@ -223,9 +290,107 @@ def create_lookup(self, filename, sp): for k, akr in enumerate(akr_): for l, rho in enumerate(rho_): interface[i, j, k, l] = PerirhizalPython.soil_root_interface_(rx, sx, akr, rho, sp) - np.savez(filename, interface=interface, rx_=rx_, sx_=sx_, akr_=akr_, rho_=rho_, soil=list(sp)) + np.savez(filename, interface = interface, rx_ = rx_, sx_ = sx_, akr_ = akr_, rho_ = rho_, soil = list(sp)) self.lookup_table = RegularGridInterpolator((rx_, sx_, akr_, rho_), interface) self.sp = sp + + def create_lookup_simp(self, filename, sp): + """ + Precomputes all soil root interface potentials for a specific soil type + and saves results into a 4D lookup table + + filename three files are written (filename, filename_, and filename_soil) + sp van genuchten soil parameters, , call + vg.create_mfp_lookup(sp) before + """ + + + # intervals for all inputs + rx_int_abs = [1.0, 16000.0] #absolute values for logarithmic scaling + sx_int_abs = [1.0, 16000.0] + akr_int = [1.0e-7,1.0e-4] + rho_int = [1.0,200.0] + + #the lookup table only needs 2 inputs from combinations of the 4 general inputs + b_func = lambda rho: 2 * (rho*rho - 1) / (1 - 0.53 * 0.53 * rho*rho + 2 * rho*rho * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] + b_0 = b_func(rho_int[0]) + b_1 = b_func(rho_int[1]) + b_int = [min(b0,b1),max(b0,b1)] + + inner_kr_bn = 200 + inner_kr_b_abs_int = [akr_int[0]/b_int[1],akr_int[1]/b_int[0]] + inner_kr_b_= np.logspace(np.log10(inner_kr_b_abs_int[0]), np.log10(inner_kr_b_abs_int[1]), inner_kr_bn) + + base_mfp_n = 200 + base_mfp_abs_int = [akr_int[0]/b_int[1]*rx_int_abs[0] + vg.fast_mfp[sp](sx_int_abs[1]),akr_int[1]/b_int[0]*rx_int_abs[1] + vg.fast_mfp[sp](sx_int_abs[0])] + base_mfp_= - np.logspace(np.log10(base_mfp_abs_int[0]), np.log10(base_mfp_abs_int[1]), base_mfp_n) + + for i, inner_kr_b in enumerate(inner_kr_b_): + print(i) + for j, base_mfp in enumerate(base_mfp_): + interface[i, j] = PerirhizalPython.soil_root_interface_simp_(inner_kr_b, base_mfp, sp) + np.savez(filename, interface=interface, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, soil=list(sp)) + self.lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp_absolute), interface) + self.sp = sp + + def create_lookup_global(self, filename, filenamevg, sp): + """ + Precomputes all soil root interface potentials for a specific soil type + and saves results into a 4D lookup table + + filename three files are written (filename, filename_, and filename_soil) + sp van genuchten soil parameters, , call + vg.create_mfp_lookup(sp) before + """ + + + # intervals for all inputs + rx_int_abs = [1.0, 16000.0] #absolute values for logarithmic scaling + sx_int_abs = [1.0, 16000.0] + akr_int = [1.0e-7,1.0e-4] + rho_int = [1.0,200.0] + vg_m_int = [0.1,1.0] + vg_Ks_int = [1.0,100.0] + vg_alpha_int = [0.01,0.3] + + #the lookup table only needs 2 inputs from combinations of the 4 general inputs + b_func = lambda rho: 2 * (rho*rho - 1) / (1 - 0.53 * 0.53 * rho*rho + 2 * rho*rho * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] + b_0 = b_func(rho_int[0]) + b_1 = b_func(rho_int[1]) + b_int = [min(b0,b1),max(b0,b1)] + + vg_m_n = 100 + vg_m_ = np.logspace(np.log10(vg_m_int[0]), np.log10(vg_m_int[1]), vg_m_n) + + sx_n = 200 + sx_ = -np.logspace(np.log10(sx_int_abs[0]), np.log10(sx_int_abs[1]), sx_n) + + #construct a smaller lookup table for the simplified + for i, vg_m in enumerate(vg_m_): + print(i) + sp_dummy = vg.Parameters([0.1,0.4,self.alpha_0,vg_m,1.0]) + vg.create_mfp_lookup(sp_dummy) + for j, sx in enumerate(sx_): + interface_vg[i, j] = vg.fast_mfp[sp_dummy](sx) + #np.savez(filename, interface=interface_vg, vg_m_=vg_m_, sx_=sx_, soil=list(sp)) + self.lookup_global_mfp = RegularGridInterpolator((inner_kr_b, base_mfp_absolute), interface) + + inner_kr_bn = 200 + inner_kr_b_abs_int = [(akr_int[0]*vg_alpha_int[0])/(self.alpha_0*b_int[1]*vg_Ks_int[1]),(akr_int[1]*vg_alpha_int[1])/(self.alpha_0*b_int[0]*vg_Ks_int[0])] + inner_kr_b_= np.logspace(np.log10(inner_kr_b_abs_int[0]), np.log10(inner_kr_b_abs_int[1]), inner_kr_bn) + + base_mfp_n = 200 + base_mfp_abs_int = [inner_kr_b_abs_int[0]*rx_int_abs[0] + 0.0,inner_kr_b_abs_int[1]*rx_int_abs[1] + self.lookup_global_mfp(vg_m_int[0],-sx_int_abs[0])] + base_mfp_= - np.logspace(np.log10(base_mfp_abs_int[0]), np.log10(base_mfp_abs_int[1]), base_mfp_n) + + for i, vg_m in enumerate(vg_m_): + print(i) + for j, inner_kr_b in enumerate(inner_kr_b_): + for k, base_mfp in enumerate(base_mfp_): + interface[i, j, k] = PerirhizalPython.soil_root_interface_global_(inner_kr_b, base_mfp, vg_m) + np.savez(filename, interface=interface, interface_vg=interface_vg, vg_m_=vg_m_, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, sx_=sx_, soil=list(sp)) + self.lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp_absolute), interface) + self.sp = sp def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): """ @@ -237,11 +402,33 @@ def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): rho geometry factor [1] f function to look up the potentials """ + + """ + the original equations were + vg.fast_mfp[sp](x) = int_(h_wilting)^(x)K(S(h))dh + k_soilfun(sx, h_sr)= (vg.fast_mfp[sp](h_sr)-vg.fast_mfp[sp](sx))/(h_sr-sx) Vanderborgth et al. 2023, Eqn [7] + (inner_kr * rx + b * sx * k_soilfun(sx, x)) / (b * k_soilfun(sx, x) + inner_kr) - x = 0 + + equivalent: + vg.fast_mfp[sp](x) + (inner_kr / b) * x - (inner_kr / b * rx + vg.fast_mfp[sp](sx)) = 0 + vg.fast_mfp[sp](x) + inner_kr_b * x + base_mfp = 0 + x only depends on 2 variables + + """ + try: sx = np.array(sx) mask = inner_kr_ == 0 inner_kr_[mask] = 1.0e-7 - rsx = self.lookup_table((rx, sx, inner_kr_, rho_)) + + #compute two inputs for the lookup table + rho = np.array(rho_) + rho2 = np.multiply(rho,rho) + b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] + inner_kr_b = np.divide(inner_kr,b) + base_mfp = - inner_kr_b * rx + vg.fast_mfp[self.sp](sx) + + rsx = self.lookup_table((inner_kr_b, base_mfp)) rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential except: if np.max(rx) > 0: @@ -279,6 +466,90 @@ def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): raise return rsx + + def soil_root_interface_potentials_global_table(self, rx, sx, inner_kr_, rho_): + """ + finds potential at the soil root interface using a globla lookup table, the lookup table works for all van Genuchten parameter sets + + rx xylem matric potential [cm] + sx bulk soil matric potential [cm] + inner_kr root radius times hydraulic conductivity [cm/day] + rho geometry factor [1] + f function to look up the potentials + """ + + """ + the original equations were + vg.fast_mfp[sp](x) = int_(h_wilting)^(x)K(S(h))dh + k_soilfun(sx, h_sr)= (vg.fast_mfp[sp](h_sr)-vg.fast_mfp[sp](sx))/(h_sr-sx) Vanderborgth et al. 2023, Eqn [7] + (inner_kr * rx + b * sx * k_soilfun(sx, x)) / (b * k_soilfun(sx, x) + inner_kr) - x = 0 + + equivalent: + vg.fast_mfp[sp](x) = alpha_0/alpha*vg_Ks * int_(h_wilting*(alpha/alpha_0))^(x*(alpha/alpha_0))K(S(alpha_0/alpha*h))/vg_Ks dh + vg.fast_mfp[sp](x) = alpha_0/alpha*vg_Ks * (vg.fast_mfp[sp_base](x*(alpha/alpha_0))-vg.fast_mfp[sp_base](h_wilt*(alpha/alpha_0))) + vg.fast_mfp[sp](x)-vg.fast_mfp[sp](sx) = alpha_0/alpha*vg.Ks * (vg.fast_mfp[sp_base](x*(alpha/alpha_0))-vg.fast_mfp[sp_base](sx*(alpha/alpha_0))) + sp_base is the van genuchten parameter set of Ks = 1 and alpha = alpha_0. Since sx*(alpha/alpha_0) should ideally be above the wilting point, choose alpha_0 large + + vg.fast_mfp[sp_base](x) + (inner_kr * alpha)/(alpha_0 * vg_Ks * b) * x - ((inner_kr * alpha)/(alpha_0 * Ks * b) * rx + vg.fast_mfp[sp_base](sx)) = 0 + vg.fast_mfp[sp_base](x) + inner_kr_b * x + base_mfp = 0 + + x only depends on 3 variables: inner_kr_b, base_mfp, vg_m (vg.fast_mfp[sp_base](x) depends only on x and this van Genuchten parameter) + + note: this function might not be particular fast as the creation of the sp_base parameter set might take a while, + + """ + + try: + sx = np.array(sx) + mask = inner_kr_ == 0 + inner_kr_[mask] = 1.0e-7 + + + #compute two inputs for the lookup table + rho = np.array(rho_) + rho2 = np.multiply(rho,rho) + b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] + inner_kr_b = np.divide(inner_kr*alpha,sp.KS*b)/self.alpha_0 + base_mfp = - ( inner_kr_b * rx + self.lookup_global_mfp(self.vg.m,sx)) + + rsx = self.global_lookup_table((inner_kr_b, base_mfp, self.vg.m)) + rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential + except: + if np.max(rx) > 0: + print("xylem matric potential positive", np.max(rx), "at", np.argmax(rx)) + if np.min(rx) < -16000: + print("xylem matric potential under -16000 cm", np.min(rx), "at", np.argmin(rx)) + if np.max(sx) > 0: + print("soil matric potential positive", np.max(sx), "at", np.argmax(sx)) + if np.min(sx) < -16000: + print("soil matric potential under -16000 cm", np.min(sx), "at", np.argmin(sx)) + if np.min(inner_kr_) < 1.0e-7: + print("radius times radial conductivity below 1.e-7", np.min(inner_kr_), "at", np.argmin(inner_kr_)) + if np.max(inner_kr_) > 1.0e-4: + print("radius times radial conductivity above 1.e-4", np.max(inner_kr_), "at", np.argmax(inner_kr_)) + if np.min(rho_) < 1: + print("geometry factor below 1", np.min(rho_), "at", np.argmin(rho_)) + if np.max(rho_) > 200: + print("geometry factor above 200", np.max(rho_), "at", np.argmax(rho_)) + + print("PerirhizalPython.soil_root_interface_potentials_table(): table look up failed, value exceeds table") + print( + "\trx", + np.min(rx), + np.max(rx), + "sx", + np.min(sx), + np.max(sx), + "inner_kr", + np.min(inner_kr_), + np.max(inner_kr_), + "rho", + np.min(rho_), + np.max(rho_), + ) + raise + + return rsx def get_density(self, type: str, volumes=None): """retrieves length, surface or volume density [cm/cm3, cm2/cm3, or cm3/cm3] per soil cells From 3b5bd8ae4283c5d5c567f57d780dd18b116fdbec Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sat, 28 Mar 2026 22:35:40 +0100 Subject: [PATCH 02/41] perirhizal waterimplementation works --- .gitignore | 3 + src/functional/Perirhizal.py | 287 +++++++++++++++++++++++++++-------- test/test_perirhizal.py | 105 +++++++++++++ 3 files changed, 335 insertions(+), 60 deletions(-) create mode 100644 test/test_perirhizal.py diff --git a/.gitignore b/.gitignore index 4a90132e1..527ec9725 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ tutorial/jupyter/.ipynb_checkpoints/ # images *.png .vscode/settings.json + +# lookup tables +*.npz diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 92553c2d9..308a38cd1 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -1,4 +1,5 @@ import numpy as np +from numpy import linalg as LA import scipy.linalg as la from scipy.interpolate import RegularGridInterpolator from scipy.optimize import fsolve, root_scalar @@ -32,15 +33,20 @@ def __init__(self, ms=None): super().__init__() self.lookup_table = None # optional 4d look up table to find soil root interface potentials + self.simp_lookup_table = None # optional 2d look up table to find soil root interface potentials + self.global_lookup_table = None # optional 3d look up table to find soil root interface potentials self.sp = None # corresponding van gencuchten soil parameter - self.alpha_0 = 0.04 # a constant that is used for numerically solving the perirhizal waterflow + self.alpha_0 = 0.3 # a constant that is used for numerically solving the perirhizal waterflow + self.h_wilt = -16000 + self.water_filename = "lookup_perirhizal_waterflow_global" #name for the global lookup table def set_soil(self, sp): """sets VG parameters, and no look up table (slow)""" vg.create_mfp_lookup(sp) self.sp = sp self.lookup_table = None + self.simp_lookup_table = None def open_lookup(self, filename): """ opens a look-up table from a file to quickly find soil root interface potentials """ @@ -56,11 +62,13 @@ def open_simp_lookup(self, filename): npzfile = np.load(filename + "_simp.npz") interface = npzfile["interface"] inner_kr_b, base_mfp = npzfile["inner_kr_b_"], npzfile["base_mfp_"] + print(inner_kr_b[0],inner_kr_b[-1], base_mfp[0],base_mfp[-1]) soil = npzfile["soil"] - self.lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp), interface) # method = "nearest" fill_value = None , bounds_error=False + self.simp_lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp), interface) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) + vg.create_mfp_lookup(self.sp) # I do not know why this has to be repeated here - def open_global_lookup(self, filename = "lookup_perirhizal_waterflow_global"): + def open_global_lookup(self, filename): """opens a global look-up table from a file to quickly find soil root interface potentials, this lookup table works for all van Genuchten parameter sets""" npzfile = np.load(filename + ".npz") interface, interface_vg = npzfile["interface"], npzfile["interface_vg"] @@ -80,13 +88,26 @@ def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): rho geometry factor [1] (outer_radius / inner_radius) """ assert len(rx) == len(sx) == len(inner_kr) == len(rho), "rx, sx, inner_kr, and rho must have the same length" + + rsx = np.array([PerirhizalPython.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], self.sp) for i in range(0, len(rx))]) + print("Norm of the computed interface potentials:", LA.norm(rsx)) + + rsx1 = 0*rsx.copy() + rsx2 = 0*rsx.copy() + rsx3 = 0*rsx.copy() + + if self.lookup_table: - rsx = self.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) - else if self.global_lookup_table: - rsx = self.soil_root_interface_potentials_global_table(rx, sx, inner_kr, rho) - else: - rsx = np.array([PerirhizalPython.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], self.sp) for i in range(0, len(rx))]) - return rsx + rsx1 = self.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) + print("Norm of the difference basic lookup table:", LA.norm(rsx-rsx1)) + if self.simp_lookup_table: + rsx2 = self.soil_root_interface_potentials_simp_table(rx, sx, inner_kr, rho) + print("Norm of the difference simp lookup table:", LA.norm(rsx-rsx2)) + if self.global_lookup_table: + rsx3 = self.soil_root_interface_potentials_global_table(rx, sx, inner_kr, rho) + print("Norm of the difference global lookup table:", LA.norm(rsx-rsx3)) + + return rsx, rsx1, rsx2, rsx3 @staticmethod def soil_root_interface_(rx, sx, inner_kr, rho, sp): @@ -99,19 +120,22 @@ def soil_root_interface_(rx, sx, inner_kr, rho, sp): rho geometry factor [1] (outer_radius / inner_radius) sp soil parameter: van Genuchten parameter set (type vg.Parameters) """ + #vg.create_mfp_lookup(sp) + #print(vg.fast_mfp[sp](-16000)) if inner_kr < 1.0e-7: return sx - k_soilfun = lambda hsoil, hint: (vg.fast_mfp[sp](hsoil) - vg.fast_mfp[sp](hint)) / (hsoil - hint) # Vanderborgth et al. 2023, Eqn [7] + k_soilfun = lambda hsoil, hint: (vg.fast_mfp[sp](hsoil) - vg.fast_mfp[sp](hint)) / (hsoil - hint + 0.001) # Vanderborgth et al. 2023, Eqn [7] rho2 = np.square(rho) # rho squared b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] fun = lambda x: (inner_kr * rx + b * sx * k_soilfun(sx, x)) / (b * k_soilfun(sx, x) + inner_kr) - x + #print(fun(rx),fun(sx-0.1),fun(sx+0.1), rho) if rx == sx: # degenerate bracket: no gradient, interface equals bulk potential - return sx + return sx rsx = root_scalar(fun, method="brentq", bracket=[min(rx, sx), max(rx, sx)]) return rsx.root @staticmethod - def soil_root_interface_simp_(inner_kr_b, base_mfp, sp): + def soil_root_interface_simp_(self, inner_kr_b, base_mfp, sp): """ finds matric potential at the soil root interface for as single segment @@ -124,13 +148,18 @@ def soil_root_interface_simp_(inner_kr_b, base_mfp, sp): inner_kr_b: inner_kr/b base_mfp: -(inner_kr/b*rx+vg.fast_mfp[sp](sx)) """ - fun = lambda x: vg.fast_mfp[sp](x) + inner_kr_b * x +base_mfp - x_int = [-16000.0,-1.0] # interval for search + fun = lambda x: vg.fast_mfp[sp](x) + inner_kr_b * (x - 0*self.h_wilt) +base_mfp + x_int = [-15999.0,-1.0] # interval for search + if fun(x_int[0])> 0: + return x_int[0] + if fun(x_int[1])< 0: + return x_int[1] + print(fun(x_int[0]),fun(x_int[1]), inner_kr_b, base_mfp) rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) return rsx.root @staticmethod - def soil_root_interface_global_(inner_kr_b, base_mfp, vg_m): + def soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m): """ finds matric potential at the soil root interface for as single segment @@ -141,7 +170,7 @@ def soil_root_interface_global_(inner_kr_b, base_mfp, vg_m): sp soil parameter: van Genuchten parameter set (type vg.Parameters) inner_kr_b: (inner_kr * alpha)/(alpha_0 * vg_Ks * b) - base_mfp: -((inner_kr * alpha)/(alpha_0 * Ks * b) * rx + vg.fast_mfp[sp_base](sx)) + base_mfp: -((inner_kr * alpha)/(alpha_0 * Ks * b) * rx + vg.fast_mfp[sp_base*(alpha_0/alpha)](sx)) vg_m: van genuchten parameter m (sp_base is the van genuchten parameter set of Ks = 1 and alpha = alpha_0, m is an input) @@ -149,9 +178,16 @@ def soil_root_interface_global_(inner_kr_b, base_mfp, vg_m): - fun = lambda x: self.lookup_global_mfp(vg_m, x) + inner_kr_b * x +base_mfp - x_int = [-16000.0,-1.0] # interval for search + fun = lambda x: self.lookup_global_mfp((vg_m, x)) + inner_kr_b * x +base_mfp + x_int = [-15990.0,-0.1] # interval for search, get closer to 0 as x = h_sr * alpha / alpha_0, alpha / alpha_0 <= 1 + if fun(x_int[0])> 0: + return x_int[0] + if fun(x_int[1])< 0: + return x_int[1] rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) + #except: + # print("f_values", fun(x_int[0]), fun(x_int[1]), vg_m) + # rsx = 0 return rsx.root @@ -204,7 +240,7 @@ def create_lookup_mpi(self, filename, sp): akrn = 100 akr_ = np.logspace(np.log10(1.0e-7), np.log10(1.0e-4), akrn) rhon = 30 - rho_ = np.logspace(np.log10(1.0), np.log10(200.0), rhon) + rho_ = np.logspace(np.log10(2.0), np.log10(200.0), rhon) if rank == 0: print(filename, "calculating", rxn * sxn * rhon * akrn, "supporting points on", size, "thread(s)") @@ -282,7 +318,7 @@ def create_lookup(self, filename, sp): akrn = 100 akr_ = np.logspace(np.log10(1.e-7), np.log10(1.e-4), akrn) rhon = 30 - rho_ = np.logspace(np.log10(1.), np.log10(200.), rhon) + rho_ = np.logspace(np.log10(2.), np.log10(200.), rhon) # TODO: talk about the lower bound here: rho = 1 leads to problems interface = np.zeros((rxn, sxn, akrn, rhon)) for i, rx in enumerate(rx_): print(i) @@ -297,46 +333,73 @@ def create_lookup(self, filename, sp): def create_lookup_simp(self, filename, sp): """ Precomputes all soil root interface potentials for a specific soil type - and saves results into a 4D lookup table + and saves results into a 2D lookup table filename three files are written (filename, filename_, and filename_soil) sp van genuchten soil parameters, , call vg.create_mfp_lookup(sp) before """ - + # intervals for all inputs - rx_int_abs = [1.0, 16000.0] #absolute values for logarithmic scaling - sx_int_abs = [1.0, 16000.0] + rx_int_abs = [1.0, 15999.0] #absolute values for logarithmic scaling + sx_int_abs = [1.0, 15999.0] akr_int = [1.0e-7,1.0e-4] - rho_int = [1.0,200.0] + rho_int = [2.0,200.0] #the lookup table only needs 2 inputs from combinations of the 4 general inputs b_func = lambda rho: 2 * (rho*rho - 1) / (1 - 0.53 * 0.53 * rho*rho + 2 * rho*rho * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] b_0 = b_func(rho_int[0]) b_1 = b_func(rho_int[1]) - b_int = [min(b0,b1),max(b0,b1)] + b_int = [min(b_0,b_1),max(b_0,b_1)] - inner_kr_bn = 200 - inner_kr_b_abs_int = [akr_int[0]/b_int[1],akr_int[1]/b_int[0]] - inner_kr_b_= np.logspace(np.log10(inner_kr_b_abs_int[0]), np.log10(inner_kr_b_abs_int[1]), inner_kr_bn) + #compute the intervals of these 2 possible inputs for the lookup table - base_mfp_n = 200 - base_mfp_abs_int = [akr_int[0]/b_int[1]*rx_int_abs[0] + vg.fast_mfp[sp](sx_int_abs[1]),akr_int[1]/b_int[0]*rx_int_abs[1] + vg.fast_mfp[sp](sx_int_abs[0])] - base_mfp_= - np.logspace(np.log10(base_mfp_abs_int[0]), np.log10(base_mfp_abs_int[1]), base_mfp_n) + inner_kr_bn = 100 + inner_kr_b_int = [akr_int[0]/b_int[1],akr_int[1]/b_int[0]] + inner_kr_b_= np.logspace(np.log10(inner_kr_b_int[0]), np.log10(inner_kr_b_int[1]), inner_kr_bn) + + print(inner_kr_b_int) + min_int, max_int = vg.fast_mfp[sp](-sx_int_abs[1]), vg.fast_mfp[sp](-sx_int_abs[0]) + print(vg.fast_mfp[sp](-sx_int_abs[1])) + print(vg.fast_mfp[sp](-sx_int_abs[0])) + base_mfp_n_1 = 500 + base_mfp_n_2 = 500 + base_mfp_n = base_mfp_n_1 + base_mfp_n_2 + base_mfp_int = [inner_kr_b_int[0]*(0*self.h_wilt-(-rx_int_abs[0])) - vg.fast_mfp[sp](-sx_int_abs[0]), inner_kr_b_int[1]*(0*self.h_wilt-(-rx_int_abs[1])) - vg.fast_mfp[sp](-sx_int_abs[1])] + #base_mfp_int are negative + #base_mfp_int[1] = min(base_mfp_int[1],-1.0e-9) #ensure positivity, this can be close to 0.0, so do not take a huge tolerance here + base_mfp_interval1= -np.logspace(np.log10(-base_mfp_int[0]), np.log10(1.0e-9), base_mfp_n_1) + base_mfp_interval2= np.logspace(np.log10(1.0e-9), np.log10(base_mfp_int[1]), base_mfp_n_2) + print(base_mfp_interval1,base_mfp_interval2) + base_mfp_ = np.concatenate((base_mfp_interval1,base_mfp_interval2)) + #base_mfp_= np.linspace(base_mfp_int[0], base_mfp_int[1], base_mfp_n) + print(base_mfp_int) + interface = np.zeros((inner_kr_bn,base_mfp_n)) + + tol = 0.001 #tolerance for extreme values of the matrix flux potential at the soil -> output h_rs = h_soil or h_x + + max_int = vg.fast_mfp[sp](-1) + min_int = vg.fast_mfp[sp](-16000+1) for i, inner_kr_b in enumerate(inner_kr_b_): print(i) for j, base_mfp in enumerate(base_mfp_): - interface[i, j] = PerirhizalPython.soil_root_interface_simp_(inner_kr_b, base_mfp, sp) - np.savez(filename, interface=interface, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, soil=list(sp)) - self.lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp_absolute), interface) + #print(base_mfp, tol - (-1-self.h_wilt)*inner_kr_b - max_int, - tol - (-16000-self.h_wilt)*inner_kr_b - min_int, min_int) + #base_mfp = max(base_mfp, tol - (-1-0*self.h_wilt)*inner_kr_b - max_int) + #base_mfp = min(base_mfp, - tol - (-16000-0*self.h_wilt)*inner_kr_b - min_int) + #print(base_mfp) + interface[i, j] = PerirhizalPython.soil_root_interface_simp_(self, inner_kr_b, base_mfp, sp) + np.savez(filename+"_simp", interface=interface, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, soil=list(sp)) + print(inner_kr_b_) + print(base_mfp_) + self.simp_lookup_table = RegularGridInterpolator((inner_kr_b_, base_mfp_), interface) self.sp = sp - def create_lookup_global(self, filename, filenamevg, sp): + def create_lookup_global(self, filename, sp): """ Precomputes all soil root interface potentials for a specific soil type - and saves results into a 4D lookup table + and saves results into a 3D lookup table filename three files are written (filename, filename_, and filename_soil) sp van genuchten soil parameters, , call @@ -348,8 +411,8 @@ def create_lookup_global(self, filename, filenamevg, sp): rx_int_abs = [1.0, 16000.0] #absolute values for logarithmic scaling sx_int_abs = [1.0, 16000.0] akr_int = [1.0e-7,1.0e-4] - rho_int = [1.0,200.0] - vg_m_int = [0.1,1.0] + rho_int = [2.0,200.0] + vg_m_int = [0.1,0.8] vg_Ks_int = [1.0,100.0] vg_alpha_int = [0.01,0.3] @@ -357,45 +420,134 @@ def create_lookup_global(self, filename, filenamevg, sp): b_func = lambda rho: 2 * (rho*rho - 1) / (1 - 0.53 * 0.53 * rho*rho + 2 * rho*rho * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] b_0 = b_func(rho_int[0]) b_1 = b_func(rho_int[1]) - b_int = [min(b0,b1),max(b0,b1)] + b_int = [min(b_0,b_1),max(b_0,b_1)] - vg_m_n = 100 + vg_m_n = 100 vg_m_ = np.logspace(np.log10(vg_m_int[0]), np.log10(vg_m_int[1]), vg_m_n) sx_n = 200 - sx_ = -np.logspace(np.log10(sx_int_abs[0]), np.log10(sx_int_abs[1]), sx_n) + sx_ = -np.logspace(np.log10(sx_int_abs[0]), np.log10(sx_int_abs[1]), sx_n) + + interface_vg = np.zeros((vg_m_n,sx_n)) #construct a smaller lookup table for the simplified for i, vg_m in enumerate(vg_m_): print(i) - sp_dummy = vg.Parameters([0.1,0.4,self.alpha_0,vg_m,1.0]) + sp_dummy = vg.Parameters([0.1,0.4,self.alpha_0,1./(1.-vg_m),1.0]) vg.create_mfp_lookup(sp_dummy) for j, sx in enumerate(sx_): - interface_vg[i, j] = vg.fast_mfp[sp_dummy](sx) + #interface_vg[i, j] = vg.fast_mfp[sp_dummy](sx) + interface_vg[i, j] = vg.matric_flux_potential(sx, sp_dummy) + print(sx, vg.matric_flux_potential(sx+15000, sp_dummy), vg.matric_flux_potential(-1, sp_dummy)) #np.savez(filename, interface=interface_vg, vg_m_=vg_m_, sx_=sx_, soil=list(sp)) - self.lookup_global_mfp = RegularGridInterpolator((inner_kr_b, base_mfp_absolute), interface) - + self.lookup_global_mfp = RegularGridInterpolator((vg_m_, sx_), interface_vg) + print(vg_m_,sx_) inner_kr_bn = 200 - inner_kr_b_abs_int = [(akr_int[0]*vg_alpha_int[0])/(self.alpha_0*b_int[1]*vg_Ks_int[1]),(akr_int[1]*vg_alpha_int[1])/(self.alpha_0*b_int[0]*vg_Ks_int[0])] + inner_kr_b_abs_int = [akr_int[0]/(b_int[1]*vg_Ks_int[1]),akr_int[1]/(b_int[0]*vg_Ks_int[0])] inner_kr_b_= np.logspace(np.log10(inner_kr_b_abs_int[0]), np.log10(inner_kr_b_abs_int[1]), inner_kr_bn) + print(inner_kr_b_abs_int) + + #base_mfp_n = 200 + base_mfp_n_1 = 100 + base_mfp_n_2 = 100 + base_mfp_n = base_mfp_n_1 + base_mfp_n_2 + print(-sx_int_abs[1] * vg_alpha_int[1]/self.alpha_0) + print(self.lookup_global_mfp((vg_m_int[0],-sx_int_abs[1] * vg_alpha_int[1]/self.alpha_0*0.1))) + print(vg.fast_mfp[sp_dummy](-10000.0)) + print("obere grenze",-sx_int_abs[0] * vg_alpha_int[0]/self.alpha_0) + print(self.lookup_global_mfp((vg_m_int[1],min(-sx_int_abs[0] * vg_alpha_int[0]/self.alpha_0,-1.0)))) + base_mfp_int = [inner_kr_b_abs_int[0]*(-(-rx_int_abs[0]) * vg_alpha_int[0]/self.alpha_0) - self.lookup_global_mfp((vg_m_int[1],min(-sx_int_abs[0] * vg_alpha_int[0]/self.alpha_0,-1))), inner_kr_b_abs_int[1]*(-(-rx_int_abs[1]) *vg_alpha_int[1]/self.alpha_0) -self.lookup_global_mfp((vg_m_int[0],-sx_int_abs[1] * vg_alpha_int[1]/self.alpha_0))] + base_mfp_interval1= -np.logspace(np.log10(-base_mfp_int[0]), np.log10(1.0e-9), base_mfp_n_1) + base_mfp_interval2= np.logspace(np.log10(1.0e-9), np.log10(base_mfp_int[1]), base_mfp_n_2) + print(base_mfp_interval1,base_mfp_interval2) + base_mfp_ = np.concatenate((base_mfp_interval1,base_mfp_interval2)) + #print(base_mfp_abs_int) + #base_mfp_= - np.logspace(np.log10(-base_mfp_abs_int[0]), np.log10(-base_mfp_abs_int[1]), base_mfp_n) + + interface = np.zeros((vg_m_n,inner_kr_bn,base_mfp_n)) + + tol = 0.001 #tolerance for extreme values of the matrix flux potential at the soil -> output h_rs = h_soil or h_x - base_mfp_n = 200 - base_mfp_abs_int = [inner_kr_b_abs_int[0]*rx_int_abs[0] + 0.0,inner_kr_b_abs_int[1]*rx_int_abs[1] + self.lookup_global_mfp(vg_m_int[0],-sx_int_abs[0])] - base_mfp_= - np.logspace(np.log10(base_mfp_abs_int[0]), np.log10(base_mfp_abs_int[1]), base_mfp_n) for i, vg_m in enumerate(vg_m_): print(i) + max_int = self.lookup_global_mfp((vg_m,-1)) + min_int = self.lookup_global_mfp((vg_m,-16000)) for j, inner_kr_b in enumerate(inner_kr_b_): for k, base_mfp in enumerate(base_mfp_): - interface[i, j, k] = PerirhizalPython.soil_root_interface_global_(inner_kr_b, base_mfp, vg_m) + #base_mfp = max(base_mfp, tol - (-1)*inner_kr_b - max_int) + #base_mfp = min(base_mfp, -tol - (-16000)*inner_kr_b - min_int) + interface[i, j, k] = PerirhizalPython.soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m) + #if base_mfp + (-1)*inner_kr_b + max_int < tol: + # interface[i, j, k] = -1 + #else: + # if base_mfp + (-16000)*inner_kr_b + min_int > tol: + # interface[i, j, k] = -16000 + # else: + # interface[i, j, k] = PerirhizalPython.soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m) np.savez(filename, interface=interface, interface_vg=interface_vg, vg_m_=vg_m_, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, sx_=sx_, soil=list(sp)) - self.lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp_absolute), interface) + print(vg_m_, inner_kr_b_, base_mfp_) + self.global_lookup_table = RegularGridInterpolator((vg_m_, inner_kr_b_, base_mfp_), interface) self.sp = sp + def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): """ finds potential at the soil root interface using a lookup table + rx xylem matric potential [cm] + sx bulk soil matric potential [cm] + inner_kr root radius times hydraulic conductivity [cm/day] + rho geometry factor [1] + f function to look up the potentials + """ + try: + sx = np.array(sx) + mask = inner_kr_ == 0 + inner_kr_[mask] = 1.0e-7 + rsx = self.lookup_table((rx, sx, inner_kr_, rho_)) + rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential + except: + if np.max(rx) > 0: + print("xylem matric potential positive", np.max(rx), "at", np.argmax(rx)) + if np.min(rx) < -16000: + print("xylem matric potential under -16000 cm", np.min(rx), "at", np.argmin(rx)) + if np.max(sx) > 0: + print("soil matric potential positive", np.max(sx), "at", np.argmax(sx)) + if np.min(sx) < -16000: + print("soil matric potential under -16000 cm", np.min(sx), "at", np.argmin(sx)) + if np.min(inner_kr_) < 1.0e-7: + print("radius times radial conductivity below 1.e-7", np.min(inner_kr_), "at", np.argmin(inner_kr_)) + if np.max(inner_kr_) > 1.0e-4: + print("radius times radial conductivity above 1.e-4", np.max(inner_kr_), "at", np.argmax(inner_kr_)) + if np.min(rho_) < 1: + print("geometry factor below 1", np.min(rho_), "at", np.argmin(rho_)) + if np.max(rho_) > 200: + print("geometry factor above 200", np.max(rho_), "at", np.argmax(rho_)) + + print("PerirhizalPython.soil_root_interface_potentials_table(): table look up failed, value exceeds table") + print( + "\trx", + np.min(rx), + np.max(rx), + "sx", + np.min(sx), + np.max(sx), + "inner_kr", + np.min(inner_kr_), + np.max(inner_kr_), + "rho", + np.min(rho_), + np.max(rho_), + ) + raise + + return rsx + + def soil_root_interface_potentials_simp_table(self, rx, sx, inner_kr_, rho_): + """ + finds potential at the soil root interface using a lookup table + rx xylem matric potential [cm] sx bulk soil matric potential [cm] inner_kr root radius times hydraulic conductivity [cm/day] @@ -425,10 +577,18 @@ def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): rho = np.array(rho_) rho2 = np.multiply(rho,rho) b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] - inner_kr_b = np.divide(inner_kr,b) - base_mfp = - inner_kr_b * rx + vg.fast_mfp[self.sp](sx) + print(b) + inner_kr_b = np.divide(inner_kr_,b) + print(inner_kr_b) + base_mfp = - (inner_kr_b * rx + vg.fast_mfp[self.sp](sx)) + print(vg.fast_mfp[self.sp](sx)) + print(base_mfp) + + #tol = 0.01 + #mask2 = base_mfp - tol > 0 + #base_mfp[mask2] = -tol - rsx = self.lookup_table((inner_kr_b, base_mfp)) + rsx = self.simp_lookup_table((inner_kr_b, base_mfp)) rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential except: if np.max(rx) > 0: @@ -509,10 +669,12 @@ def soil_root_interface_potentials_global_table(self, rx, sx, inner_kr_, rho_): rho = np.array(rho_) rho2 = np.multiply(rho,rho) b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] - inner_kr_b = np.divide(inner_kr*alpha,sp.KS*b)/self.alpha_0 - base_mfp = - ( inner_kr_b * rx + self.lookup_global_mfp(self.vg.m,sx)) - - rsx = self.global_lookup_table((inner_kr_b, base_mfp, self.vg.m)) + #print("inner_kr1",inner_kr_[0]*self.sp.alpha/self.sp.Ksat/b[0]/self.alpha_0) + inner_kr_b = np.divide(inner_kr_,self.sp.Ksat*b) + base_mfp = - ( inner_kr_b * rx * self.sp.alpha / self.alpha_0 + self.lookup_global_mfp((self.sp.m,sx*self.sp.alpha/self.alpha_0))) + print((inner_kr_b, base_mfp, self.sp.m)) #TODO multiply by alpha or something + rsx = np.array([self.global_lookup_table((self.sp.m, inner_kr_b[i], base_mfp[i])) for i in range(len(inner_kr_b))])*self.alpha_0/self.sp.alpha + print(mask, rsx) rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential except: if np.max(rx) > 0: @@ -958,7 +1120,12 @@ def get_default_volumes_(self): sp = vg.Parameters(hydrus_loam) vg.create_mfp_lookup(sp) peri = PerirhizalPython() - peri.create_lookup_mpi(filename, sp) # takes some hours; mpiexec -n 4 python Perirhizal.py + #peri.create_lookup_mpi(filename, sp) # takes some hours; mpiexec -n 4 python Perirhizal.py + peri.create_lookup_simp(filename, sp) # should be better + peri.create_lookup_global(peri.water_filename) # this is global + + # generate some tests + # # peri.open_lookup(filename) # peri.set_soil(vg.Parameters(loam)) diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py new file mode 100644 index 000000000..0fbcb7545 --- /dev/null +++ b/test/test_perirhizal.py @@ -0,0 +1,105 @@ + +# this is a small test file by Erik Kopp to test the alternative implementation of the perirhizal resistances and the perirhizal diffusion + + +import sys; sys.path.append("../src/functional") +#from plantbox import Perirhizal +from plantbox.functional.Perirhizal import PerirhizalPython +import plantbox.functional.van_genuchten as vg +import Perirhizal +import pandas as pd +import numpy as np +import time + +# generate random inputs for one perirhizal segment + +""" + rx xylem matric potential [cm] + sx bulk soil matric potential [cm] + inner_kr root radius times hydraulic conductivity [cm/day] + rho geometry factor [1] (outer_radius / inner_radius) + sp soil parameter: van Genuchten parameter set (type vg.Parameters) + sp_theta_r + sp_theta_s + sp_n + sp_alpha + sp_Ksat +""" +labels = ["rx","sx","inner_kr","rho","hsr","hsr_base","hsr_simp","hsr_global"] + +#kr is assumed to lie between .5 and 5e-6 cm/hPa/d = 0.5 to 5e-6 1/d +#r is assumed to be 0.03 +#multiply this number by 10 in order to not fall under the threshhold at which the soil hydraulic potential is chosen as a default + +Intervals = { + "rx": [-14000,-10], + "sx": [-1000,-10], + "inner_kr": [1.5e-7,1.5e-6], + "rho": [10,199.0], + } +Intervals = pd.DataFrame(Intervals, index = ["min","max"]) + +ntests = 10 +tests = pd.DataFrame(index = range(ntests),columns = labels) + +for one_label in Intervals.columns: + min_val = Intervals.loc["min",one_label] + max_val = Intervals.loc["max",one_label] + + testvalues = (max_val - min_val) * np.random.rand(ntests) + min_val * np.ones(ntests) + tests[one_label] = testvalues + +peri = PerirhizalPython() +#peri = PerirhizalPython(Perirhizal) + + +rx = np.array(tests.loc[:,"rx"]) #* (-1) +sx = np.array(tests.loc[:,"sx"]) #* (-1) +inner_kr = np.array(tests.loc[:,"inner_kr"]) +rho = np.array(tests.loc[:,"rho"]) + +hydrus_loam = [0.078, 0.43, 0.036, 1.56, 24.96] +sp = vg.Parameters(hydrus_loam) +peri.set_soil(sp) + +#create the big lookup table +t = time.time() +peri.create_lookup_mpi("hydrus_loam", sp) +elapsed_time = time.time() - t +print("Creating the big lookup table took about ", elapsed_time) # + +peri.open_lookup("hydrus_loam") + +#create the small lookup table +#t = time.time() +#peri.create_lookup_simp("hydrus_loam", sp) +#elapsed_time = time.time() - t + +peri.open_simp_lookup("hydrus_loam") +#print("Creating the simp lookup table took about ", elapsed_time) # + +#create the global lookup table +#t = time.time() +#peri.create_lookup_global(peri.water_filename, sp) +#elapsed_time = time.time() - t + +peri.open_global_lookup(peri.water_filename) +#print("Creating the global lookup table took about ", elapsed_time) # + +hsr, hsr_base, hsr_simp, hsr_global = peri.soil_root_interface_potentials(rx, sx, inner_kr, rho) + +tests.loc[:,"hsr"] = hsr + +#the standard implementation +tests.loc[:,"hsr_base"] = hsr_base + +#the first simplified implementation +tests.loc[:,"hsr_simp"] = hsr_simp + +#the alternative implementation +tests.loc[:,"hsr_global"] = hsr_global + + + +print(tests) + From 8a8081e398d61419fa8ab8277bd933b75391f158 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sun, 29 Mar 2026 10:23:00 +0200 Subject: [PATCH 03/41] verbose debugging removed --- src/functional/Perirhizal.py | 68 ++++-------------------------------- test/test_perirhizal.py | 10 +++--- 2 files changed, 12 insertions(+), 66 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 308a38cd1..086513edd 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -62,7 +62,6 @@ def open_simp_lookup(self, filename): npzfile = np.load(filename + "_simp.npz") interface = npzfile["interface"] inner_kr_b, base_mfp = npzfile["inner_kr_b_"], npzfile["base_mfp_"] - print(inner_kr_b[0],inner_kr_b[-1], base_mfp[0],base_mfp[-1]) soil = npzfile["soil"] self.simp_lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp), interface) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) @@ -154,7 +153,6 @@ def soil_root_interface_simp_(self, inner_kr_b, base_mfp, sp): return x_int[0] if fun(x_int[1])< 0: return x_int[1] - print(fun(x_int[0]),fun(x_int[1]), inner_kr_b, base_mfp) rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) return rsx.root @@ -185,9 +183,6 @@ def soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m): if fun(x_int[1])< 0: return x_int[1] rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) - #except: - # print("f_values", fun(x_int[0]), fun(x_int[1]), vg_m) - # rsx = 0 return rsx.root @@ -359,40 +354,23 @@ def create_lookup_simp(self, filename, sp): inner_kr_b_int = [akr_int[0]/b_int[1],akr_int[1]/b_int[0]] inner_kr_b_= np.logspace(np.log10(inner_kr_b_int[0]), np.log10(inner_kr_b_int[1]), inner_kr_bn) - print(inner_kr_b_int) min_int, max_int = vg.fast_mfp[sp](-sx_int_abs[1]), vg.fast_mfp[sp](-sx_int_abs[0]) - print(vg.fast_mfp[sp](-sx_int_abs[1])) - print(vg.fast_mfp[sp](-sx_int_abs[0])) base_mfp_n_1 = 500 base_mfp_n_2 = 500 base_mfp_n = base_mfp_n_1 + base_mfp_n_2 base_mfp_int = [inner_kr_b_int[0]*(0*self.h_wilt-(-rx_int_abs[0])) - vg.fast_mfp[sp](-sx_int_abs[0]), inner_kr_b_int[1]*(0*self.h_wilt-(-rx_int_abs[1])) - vg.fast_mfp[sp](-sx_int_abs[1])] - #base_mfp_int are negative - #base_mfp_int[1] = min(base_mfp_int[1],-1.0e-9) #ensure positivity, this can be close to 0.0, so do not take a huge tolerance here + #base_mfp_int are negative and positive base_mfp_interval1= -np.logspace(np.log10(-base_mfp_int[0]), np.log10(1.0e-9), base_mfp_n_1) base_mfp_interval2= np.logspace(np.log10(1.0e-9), np.log10(base_mfp_int[1]), base_mfp_n_2) - print(base_mfp_interval1,base_mfp_interval2) base_mfp_ = np.concatenate((base_mfp_interval1,base_mfp_interval2)) - #base_mfp_= np.linspace(base_mfp_int[0], base_mfp_int[1], base_mfp_n) - print(base_mfp_int) - interface = np.zeros((inner_kr_bn,base_mfp_n)) - - tol = 0.001 #tolerance for extreme values of the matrix flux potential at the soil -> output h_rs = h_soil or h_x - max_int = vg.fast_mfp[sp](-1) - min_int = vg.fast_mfp[sp](-16000+1) + interface = np.zeros((inner_kr_bn,base_mfp_n)) for i, inner_kr_b in enumerate(inner_kr_b_): print(i) for j, base_mfp in enumerate(base_mfp_): - #print(base_mfp, tol - (-1-self.h_wilt)*inner_kr_b - max_int, - tol - (-16000-self.h_wilt)*inner_kr_b - min_int, min_int) - #base_mfp = max(base_mfp, tol - (-1-0*self.h_wilt)*inner_kr_b - max_int) - #base_mfp = min(base_mfp, - tol - (-16000-0*self.h_wilt)*inner_kr_b - min_int) - #print(base_mfp) interface[i, j] = PerirhizalPython.soil_root_interface_simp_(self, inner_kr_b, base_mfp, sp) np.savez(filename+"_simp", interface=interface, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, soil=list(sp)) - print(inner_kr_b_) - print(base_mfp_) self.simp_lookup_table = RegularGridInterpolator((inner_kr_b_, base_mfp_), interface) self.sp = sp @@ -430,39 +408,29 @@ def create_lookup_global(self, filename, sp): interface_vg = np.zeros((vg_m_n,sx_n)) - #construct a smaller lookup table for the simplified + #construct a smaller lookup table for the simplified matrix flux potential for i, vg_m in enumerate(vg_m_): print(i) sp_dummy = vg.Parameters([0.1,0.4,self.alpha_0,1./(1.-vg_m),1.0]) vg.create_mfp_lookup(sp_dummy) for j, sx in enumerate(sx_): - #interface_vg[i, j] = vg.fast_mfp[sp_dummy](sx) interface_vg[i, j] = vg.matric_flux_potential(sx, sp_dummy) - print(sx, vg.matric_flux_potential(sx+15000, sp_dummy), vg.matric_flux_potential(-1, sp_dummy)) - #np.savez(filename, interface=interface_vg, vg_m_=vg_m_, sx_=sx_, soil=list(sp)) self.lookup_global_mfp = RegularGridInterpolator((vg_m_, sx_), interface_vg) - print(vg_m_,sx_) + + inner_kr_bn = 200 inner_kr_b_abs_int = [akr_int[0]/(b_int[1]*vg_Ks_int[1]),akr_int[1]/(b_int[0]*vg_Ks_int[0])] inner_kr_b_= np.logspace(np.log10(inner_kr_b_abs_int[0]), np.log10(inner_kr_b_abs_int[1]), inner_kr_bn) - print(inner_kr_b_abs_int) #base_mfp_n = 200 base_mfp_n_1 = 100 base_mfp_n_2 = 100 base_mfp_n = base_mfp_n_1 + base_mfp_n_2 - print(-sx_int_abs[1] * vg_alpha_int[1]/self.alpha_0) - print(self.lookup_global_mfp((vg_m_int[0],-sx_int_abs[1] * vg_alpha_int[1]/self.alpha_0*0.1))) - print(vg.fast_mfp[sp_dummy](-10000.0)) - print("obere grenze",-sx_int_abs[0] * vg_alpha_int[0]/self.alpha_0) - print(self.lookup_global_mfp((vg_m_int[1],min(-sx_int_abs[0] * vg_alpha_int[0]/self.alpha_0,-1.0)))) + #base_mfp_int are negative and positive base_mfp_int = [inner_kr_b_abs_int[0]*(-(-rx_int_abs[0]) * vg_alpha_int[0]/self.alpha_0) - self.lookup_global_mfp((vg_m_int[1],min(-sx_int_abs[0] * vg_alpha_int[0]/self.alpha_0,-1))), inner_kr_b_abs_int[1]*(-(-rx_int_abs[1]) *vg_alpha_int[1]/self.alpha_0) -self.lookup_global_mfp((vg_m_int[0],-sx_int_abs[1] * vg_alpha_int[1]/self.alpha_0))] base_mfp_interval1= -np.logspace(np.log10(-base_mfp_int[0]), np.log10(1.0e-9), base_mfp_n_1) base_mfp_interval2= np.logspace(np.log10(1.0e-9), np.log10(base_mfp_int[1]), base_mfp_n_2) - print(base_mfp_interval1,base_mfp_interval2) base_mfp_ = np.concatenate((base_mfp_interval1,base_mfp_interval2)) - #print(base_mfp_abs_int) - #base_mfp_= - np.logspace(np.log10(-base_mfp_abs_int[0]), np.log10(-base_mfp_abs_int[1]), base_mfp_n) interface = np.zeros((vg_m_n,inner_kr_bn,base_mfp_n)) @@ -471,22 +439,10 @@ def create_lookup_global(self, filename, sp): for i, vg_m in enumerate(vg_m_): print(i) - max_int = self.lookup_global_mfp((vg_m,-1)) - min_int = self.lookup_global_mfp((vg_m,-16000)) for j, inner_kr_b in enumerate(inner_kr_b_): for k, base_mfp in enumerate(base_mfp_): - #base_mfp = max(base_mfp, tol - (-1)*inner_kr_b - max_int) - #base_mfp = min(base_mfp, -tol - (-16000)*inner_kr_b - min_int) interface[i, j, k] = PerirhizalPython.soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m) - #if base_mfp + (-1)*inner_kr_b + max_int < tol: - # interface[i, j, k] = -1 - #else: - # if base_mfp + (-16000)*inner_kr_b + min_int > tol: - # interface[i, j, k] = -16000 - # else: - # interface[i, j, k] = PerirhizalPython.soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m) np.savez(filename, interface=interface, interface_vg=interface_vg, vg_m_=vg_m_, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, sx_=sx_, soil=list(sp)) - print(vg_m_, inner_kr_b_, base_mfp_) self.global_lookup_table = RegularGridInterpolator((vg_m_, inner_kr_b_, base_mfp_), interface) self.sp = sp @@ -577,16 +533,8 @@ def soil_root_interface_potentials_simp_table(self, rx, sx, inner_kr_, rho_): rho = np.array(rho_) rho2 = np.multiply(rho,rho) b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] - print(b) inner_kr_b = np.divide(inner_kr_,b) - print(inner_kr_b) base_mfp = - (inner_kr_b * rx + vg.fast_mfp[self.sp](sx)) - print(vg.fast_mfp[self.sp](sx)) - print(base_mfp) - - #tol = 0.01 - #mask2 = base_mfp - tol > 0 - #base_mfp[mask2] = -tol rsx = self.simp_lookup_table((inner_kr_b, base_mfp)) rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential @@ -669,12 +617,10 @@ def soil_root_interface_potentials_global_table(self, rx, sx, inner_kr_, rho_): rho = np.array(rho_) rho2 = np.multiply(rho,rho) b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] - #print("inner_kr1",inner_kr_[0]*self.sp.alpha/self.sp.Ksat/b[0]/self.alpha_0) inner_kr_b = np.divide(inner_kr_,self.sp.Ksat*b) base_mfp = - ( inner_kr_b * rx * self.sp.alpha / self.alpha_0 + self.lookup_global_mfp((self.sp.m,sx*self.sp.alpha/self.alpha_0))) - print((inner_kr_b, base_mfp, self.sp.m)) #TODO multiply by alpha or something + #lookup table rsx = np.array([self.global_lookup_table((self.sp.m, inner_kr_b[i], base_mfp[i])) for i in range(len(inner_kr_b))])*self.alpha_0/self.sp.alpha - print(mask, rsx) rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential except: if np.max(rx) > 0: diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 0fbcb7545..90cc8083d 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -39,7 +39,7 @@ } Intervals = pd.DataFrame(Intervals, index = ["min","max"]) -ntests = 10 +ntests = 30 tests = pd.DataFrame(index = range(ntests),columns = labels) for one_label in Intervals.columns: @@ -63,10 +63,10 @@ peri.set_soil(sp) #create the big lookup table -t = time.time() -peri.create_lookup_mpi("hydrus_loam", sp) -elapsed_time = time.time() - t -print("Creating the big lookup table took about ", elapsed_time) # +#t = time.time() +#peri.create_lookup_mpi("hydrus_loam", sp) +#elapsed_time = time.time() - t +#print("Creating the big lookup table took about ", elapsed_time) # peri.open_lookup("hydrus_loam") From 15ed00655e5be04169dc3d95c673118bcb875f2a Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sun, 29 Mar 2026 12:58:25 +0200 Subject: [PATCH 04/41] added tutorial --- tutorial/chapter7_coupled/example7_3_coupling_fp.py | 2 +- tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py | 2 +- tutorial/chapter7_coupled/example7_3_lookup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tutorial/chapter7_coupled/example7_3_coupling_fp.py b/tutorial/chapter7_coupled/example7_3_coupling_fp.py index e22f8a0d5..259f20177 100644 --- a/tutorial/chapter7_coupled/example7_3_coupling_fp.py +++ b/tutorial/chapter7_coupled/example7_3_coupling_fp.py @@ -78,7 +78,7 @@ def picker(x, y, z): # Perirhizal initialization peri = PerirhizalPython(hm.ms) # |\label{l73c:peri}| # peri.set_soil(vg.Parameters(loam)) # |\label{l73c:perisoil}| -peri.open_lookup("results/hydrus_loam") # |\label{l73c:peritable}| +peri.open_simp_lookup("results/hydrus_loam") # |\label{l73c:peritable}| outer_r = peri.get_outer_radii("length") # |\label{l73c:outer}| inner_r = peri.ms.radii diff --git a/tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py b/tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py index 8ea092603..7b14e3ce7 100644 --- a/tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py +++ b/tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py @@ -87,7 +87,7 @@ def picker(x, y, z): peri = PerirhizalPython(hm.ms) # |\label{l73c:peri}| # peri.set_soil(vg.Parameters(loam)) # |\label{l73c:perisoil}| home = Path.home() - peri.open_lookup("results/hydrus_loam") # |\label{l73c:peritable}| + peri.open_simp_lookup("results/hydrus_loam") # |\label{l73c:peritable}| outer_r = peri.get_outer_radii("length") # |\label{l73c:outer}| inner_r = peri.ms.radii diff --git a/tutorial/chapter7_coupled/example7_3_lookup.py b/tutorial/chapter7_coupled/example7_3_lookup.py index 7e575d680..932d924ad 100644 --- a/tutorial/chapter7_coupled/example7_3_lookup.py +++ b/tutorial/chapter7_coupled/example7_3_lookup.py @@ -9,4 +9,4 @@ filename = "hydrus_loam" sp = vg.Parameters(hydrus_loam) # |\label{l73l:soil_end}| vg.create_mfp_lookup(sp) # |\label{l73l:mfp}| -peri.create_lookup_mpi("results/" + filename, sp) # |\label{l73l:lookup}| +peri.create_lookup_simp("results/" + filename, sp) # |\label{l73l:lookup}| From bdf709f529b4682d205cdd47407145ef0bacad77 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Fri, 17 Apr 2026 09:30:00 +0200 Subject: [PATCH 05/41] upload after meeting --- src/functional/Perirhizal.py | 211 +++++++++++++++++++++++++++++++---- test/test_perirhizal.py | 163 +++++++++++++++++++++++++-- 2 files changed, 344 insertions(+), 30 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 086513edd..f903b42b5 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -4,6 +4,9 @@ from scipy.interpolate import RegularGridInterpolator from scipy.optimize import fsolve, root_scalar from scipy.spatial import ConvexHull, Voronoi +from scipy import integrate + +import math import plantbox as pb import plantbox.functional.van_genuchten as vg @@ -35,6 +38,7 @@ def __init__(self, ms=None): self.lookup_table = None # optional 4d look up table to find soil root interface potentials self.simp_lookup_table = None # optional 2d look up table to find soil root interface potentials self.global_lookup_table = None # optional 3d look up table to find soil root interface potentials + self.lookup_table_solutes = None self.sp = None # corresponding van gencuchten soil parameter self.alpha_0 = 0.3 # a constant that is used for numerically solving the perirhizal waterflow @@ -66,7 +70,7 @@ def open_simp_lookup(self, filename): self.simp_lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp), interface) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) vg.create_mfp_lookup(self.sp) # I do not know why this has to be repeated here - + def open_global_lookup(self, filename): """opens a global look-up table from a file to quickly find soil root interface potentials, this lookup table works for all van Genuchten parameter sets""" npzfile = np.load(filename + ".npz") @@ -75,8 +79,18 @@ def open_global_lookup(self, filename): self.global_lookup_table = RegularGridInterpolator((vg_m_, inner_kr_b_, base_mfp_), interface) # method = "nearest" fill_value = None , bounds_error=False self.lookup_global_mfp = RegularGridInterpolator((vg_m_, sx_), interface_vg) #.sp = vg.Parameters(soil) - - def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): + + def open_lookup_solutes(self, filename): + """opens a look-up table from a file to quickly find soil root solute concentrations in the steady state case""" + npzfile = np.load(filename + ".npz") + F_integral = npzfile["F_integral"] + base_mfp_ = npzfile["base_mfp_"] + soil = npzfile["soil"] + self.lookup_table_sssolutes = RegularGridInterpolator((base_mfp_), F_integral) # method = "nearest" fill_value = None , bounds_error=False + self.sp = vg.Parameters(soil) + vg.create_mfp_lookup(self.sp) # I do not know why this has to be repeated here + + def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): #TODO: change this after the debugging is done """ finds matric potentials at the soil root interface for as all segments uses a look up tables if present (see create_lookup, and open_lookup) @@ -88,25 +102,20 @@ def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): """ assert len(rx) == len(sx) == len(inner_kr) == len(rho), "rx, sx, inner_kr, and rho must have the same length" - rsx = np.array([PerirhizalPython.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], self.sp) for i in range(0, len(rx))]) - print("Norm of the computed interface potentials:", LA.norm(rsx)) - - rsx1 = 0*rsx.copy() - rsx2 = 0*rsx.copy() - rsx3 = 0*rsx.copy() - if self.lookup_table: - rsx1 = self.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) - print("Norm of the difference basic lookup table:", LA.norm(rsx-rsx1)) - if self.simp_lookup_table: - rsx2 = self.soil_root_interface_potentials_simp_table(rx, sx, inner_kr, rho) - print("Norm of the difference simp lookup table:", LA.norm(rsx-rsx2)) - if self.global_lookup_table: - rsx3 = self.soil_root_interface_potentials_global_table(rx, sx, inner_kr, rho) - print("Norm of the difference global lookup table:", LA.norm(rsx-rsx3)) + rsx = self.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) + else: + if self.simp_lookup_table: + rsx = self.soil_root_interface_potentials_table_simp(rx, sx, inner_kr, rho) + else: + if self.global_lookup_table: + rsx = self.soil_root_interface_potentials_table_global(rx, sx, inner_kr, rho) + else: + rsx = np.array([PerirhizalPython.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], self.sp) for i in range(0, len(rx))]) + - return rsx, rsx1, rsx2, rsx3 + return rsx @staticmethod def soil_root_interface_(rx, sx, inner_kr, rho, sp): @@ -127,7 +136,7 @@ def soil_root_interface_(rx, sx, inner_kr, rho, sp): rho2 = np.square(rho) # rho squared b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] fun = lambda x: (inner_kr * rx + b * sx * k_soilfun(sx, x)) / (b * k_soilfun(sx, x) + inner_kr) - x - #print(fun(rx),fun(sx-0.1),fun(sx+0.1), rho) + if rx == sx: # degenerate bracket: no gradient, interface equals bulk potential return sx rsx = root_scalar(fun, method="brentq", bracket=[min(rx, sx), max(rx, sx)]) @@ -184,7 +193,143 @@ def soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m): return x_int[1] rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) return rsx.root + + def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): + """ + finds solute concentration at the soil root interface for all segments assuming steady state + uses a look up tables if present (see create_lookup, and open_lookup) + + Phi_root matrix flux potential at the root-soil-interface [cm2/d] + Phi_soil matrix flux potential at the bulk soil [cm2/d] + c_bulk solute concentration at the bulk soil [mol/cm3] + Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] + Km half saturation constant Michaelis Menten Kinetics [mol/cm3] + Ds Diffusion constant in water [cm2/d] + waterflow steady state waterflow (entire root circumference) [cm2/d] + sp van Genuchten parameter set + """ + assert len(Phi_root) == len(Phi_soil) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" + + n_segments = len(c_bulk) + + rsc = np.zeros(n_segments) + F = np.zeros(n_segments) #F is a helper values + F_tilde = np.zeros(n_segments) + + if self.lookup_table_solutes: + F=[(self.lookup_table_solutes(Phi_soil[i])-self.lookup_table_solutes(Phi_root[i])) for i in range(0, len(c_bulk))] + else: + F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] + + #compute a prefactor + D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) + + #solve quadratic eqation # TODO: Link to publication + for i in range(n_segments): + F_tilde[i]=math.exp(D_tilde*F[i]) + a1=c_bulk[i]/F_tilde[i] + a2=(1-F_tilde[i])/(F_tilde[i]*waterflow[i]) + p=Km[i]-a2*Vmax[i]-a1 + q=-Km[i]*a1 + rsc[i]=math.sqrt(pow(p/2,2)-q)-p/2 + + + return rsc + + def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, waterflow, sp): + """ + finds solute concentration at the soil root interface for all segments assuming steady r + uses a look up tables if present (see create_lookup, and open_lookup) + + Phi_root matrix flux potential at the root-soil-interface [cm2/d] + Phi_soil matrix flux potential at the bulk soil [cm2/d] + rho geometry factor [1] (outer_radius / inner_radius) + c_bulk solute concentration at the bulk soil [mol/cm3] + Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] + Km half saturation constant Michaelis Menten Kinetics [mol/cm3] + Ds Diffusion constant in water [cm2/d] + waterflow steady state waterflow (entire root circumference) [cm2/d] + sp van Genuchten parameter set + """ + assert len(Phi_root) == len(Phi_soil) == len(rho) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" + + n_segments = len(c_bulk) + rsc = np.zeros(n_segments) + F = np.zeros(n_segments) #F is a helper values + F_tilde = np.zeros(n_segments) + R_sr = np.zeros(n_segments) + + if self.lookup_table_solutes: + F=[(self.lookup_table_solutes(Phi_soil[i])-self.lookup_table_solutes(Phi_root[i])) for i in range(0, len(c_bulk))] + else: + F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] + + + + #compute a prefactor + D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) + C_d = self.alpha_0*self.sp.Ksat/sp.alpha*D_tilde + + + + #solve quadratic eqation # TODO: Link to publication + for i in range(n_segments): + R_sr[i] = self.integral_overconcentration_(Phi_root[i], Phi_soil[i], rho[i], C_d, self.sp) + F_tilde[i]=math.exp(D_tilde*F[i]) + a1=c_bulk[i]/(F_tilde[i]*R_sr[i]) #*vg.water_content(vg.fast_imfp[sp](Phi_soil), self.sp) + a2=(1-F_tilde[i]*R_sr[i])/(F_tilde[i]*R_sr[i]*waterflow[i]) + p=Km[i]-a2*Vmax[i]-a1 + q=-Km[i]*a1 + rsc[i]=math.sqrt(pow(p/2,2)-q)-p/2 + + + return rsc + + @staticmethod + def integral_overDiffusion_(Phi, sp): + """ + Integral of (D_s(theta_s-theta_r)^(13/3))/(theta*D(theta)) from Phi = 0 to Phi + + Phi matrix flux potential [cm2/d] + sp soil parameter: van Genuchten parameter set (type vg.Parameters) + """ + if Phi <=0: + return 0 + theta_rel = sp.theta_R/(sp.theta_S-sp.theta_R) + integral_fun = lambda Phi: pow(theta_rel+vg.effective_saturation(vg.fast_imfp[sp](Phi),sp),-13/3) + integral_overD, _ = integrate.quad(integral_fun, 0, Phi) + + return integral_overD + + def integral_overconcentration_(self, Phi_in, Phi_out, rho, C_d, sp): + """ + (2 pi * 0.9999 rprhiz^2 (c_rs-C_SW))/(Integral from 0.01*rprhiz to rprhiz of 2 pi s (c(s)-C_SW) ds) + + Phi_in matrix flux potential at 0.01 r_prhiz [cm2/d] + Phi_out matrix flux potential at r_prhiz [cm2/d] + rho r_prhiz / r_root + C_d (alpha_0 K_s / alpha) (theta_s^2/(D_s (theta_s-theta_r)^(13/3))) + + Assume Phi(s=r/r-prhiz)=As^2-2Aln(s)+C (steady rate) + A=(Phi_in-Phi_out)/(0.01^2-2*ln(0.01)-1) + C=Phi_out-A + """ + + A = (Phi_in-Phi_out)/(1/(rho*rho)-np.log(1/(rho*rho))-1) + C = Phi_out-A + + + if self.lookup_table_solutes: + F0=self.lookup_table_solutes(A*(1/(rho*rho)-np.log(1/(rho*rho)))+C) + integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](A*(s2-np.log(s2))+C), self.sp)*math.exp(C_d*(self.lookup_table_solutes(A*(s2-np.log(s2))+C)-F0)) + else: + F0=self.integral_overDiffusion_(A*(1/(rho*rho)-np.log(1/(rho*rho)))+C, self.sp) + integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](A*(s2-np.log(s2))+C), self.sp)*math.exp(C_d*(self.integral_overDiffusion_(A*(s2-np.log(s2))+C, self.sp)-F0)) + R_sr, _ = integrate.quad(integral_fun, 1/(rho*rho), 1.0) + + return R_sr + def perirhizal_conductance_per_layer(self, h_bs, h_sr, sp): """ @@ -445,7 +590,27 @@ def create_lookup_global(self, filename, sp): np.savez(filename, interface=interface, interface_vg=interface_vg, vg_m_=vg_m_, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, sx_=sx_, soil=list(sp)) self.global_lookup_table = RegularGridInterpolator((vg_m_, inner_kr_b_, base_mfp_), interface) self.sp = sp - + + def create_integralDiffusion_lookup(self, filename, sp): + """ + Precomputes all integrals for the steady state solute flow + + Phi upper matrix flux potential (bottom is set to 0) [cm2/d] + sp van genuchten soil parameters, , call + vg.create_mfp_lookup(sp) before + """ + Phin = 300 + Phi_ = np.logspace(np.log10(1.0e-6), np.log10(300), Phin) + base_mfp_=Phi_ + integral_overD = np.zeros((Phin,2)) + for i, Phi in enumerate(Phi_): + print(i) + integral_overD[i,0] = PerirhizalPython.integral_overDiffusion_(Phi, sp) + integral_overD[i,1] = integral_overD[i,0] + np.savez(filename, integral_overD = integral_overD, base_mfp_ = base_mfp_, soil = list(sp)) + self.lookup_table_solutes = RegularGridInterpolator((base_mfp_, np.array([0,1])) , integral_overD) + self.sp = sp + def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): """ @@ -500,7 +665,7 @@ def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): return rsx - def soil_root_interface_potentials_simp_table(self, rx, sx, inner_kr_, rho_): + def soil_root_interface_potentials_table_simp(self, rx, sx, inner_kr_, rho_): """ finds potential at the soil root interface using a lookup table @@ -575,7 +740,7 @@ def soil_root_interface_potentials_simp_table(self, rx, sx, inner_kr_, rho_): return rsx - def soil_root_interface_potentials_global_table(self, rx, sx, inner_kr_, rho_): + def soil_root_interface_potentials_table_global(self, rx, sx, inner_kr_, rho_): """ finds potential at the soil root interface using a globla lookup table, the lookup table works for all van Genuchten parameter sets diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 90cc8083d..083331203 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -5,7 +5,12 @@ import sys; sys.path.append("../src/functional") #from plantbox import Perirhizal from plantbox.functional.Perirhizal import PerirhizalPython +from numpy import linalg as LA import plantbox.functional.van_genuchten as vg +from rosi.richards_flat import RichardsFlatWrapper as RichardsWrapper # Python part, macroscopic soil model +#from richards import RichardsWrapper # Python part +#from richards_no_mpi import RichardsNoMPIWrapper # Python part of cylindrcial +from rosi.rosi_richardsnc_cyl import RichardsNCCylFoam # C++ part (Dumux binding), macroscopic soil model import Perirhizal import pandas as pd import numpy as np @@ -18,6 +23,9 @@ sx bulk soil matric potential [cm] inner_kr root radius times hydraulic conductivity [cm/day] rho geometry factor [1] (outer_radius / inner_radius) + c_sol concentration of solutes in the bulk soil + Vmax Michaelis Menten Kinetics maximal solute uptake rate + Km Michaelis Menten Kinetics half saturation sp soil parameter: van Genuchten parameter set (type vg.Parameters) sp_theta_r sp_theta_s @@ -25,7 +33,12 @@ sp_alpha sp_Ksat """ -labels = ["rx","sx","inner_kr","rho","hsr","hsr_base","hsr_simp","hsr_global"] +labels = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km","hsr","hsr_base","hsr_simp","hsr_global","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] +#for now only water flow: +#labels = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km","hsr","hsr_1d","hsr_base","hsr_simp","hsr_global"] + +inputs = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km"] +outputs = ["hsr","hsr_base","hsr_simp","hsr_global","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] #kr is assumed to lie between .5 and 5e-6 cm/hPa/d = 0.5 to 5e-6 1/d #r is assumed to be 0.03 @@ -36,6 +49,9 @@ "sx": [-1000,-10], "inner_kr": [1.5e-7,1.5e-6], "rho": [10,199.0], + "c_sol": [1.0e-9,1.0e-5], + "Vmax": [1.0e-11,1.0e-10], + "Km": [1.0e-8,1.0e-6], } Intervals = pd.DataFrame(Intervals, index = ["min","max"]) @@ -57,6 +73,10 @@ sx = np.array(tests.loc[:,"sx"]) #* (-1) inner_kr = np.array(tests.loc[:,"inner_kr"]) rho = np.array(tests.loc[:,"rho"]) +c_sol = np.array(tests.loc[:,"c_sol"]) +Vmax = np.array(tests.loc[:,"Vmax"]) +Km = np.array(tests.loc[:,"Km"]) +Ds = 1.0e2 hydrus_loam = [0.078, 0.43, 0.036, 1.56, 24.96] sp = vg.Parameters(hydrus_loam) @@ -72,7 +92,7 @@ #create the small lookup table #t = time.time() -#peri.create_lookup_simp("hydrus_loam", sp) +peri.create_lookup_simp("hydrus_loam", sp) #elapsed_time = time.time() - t peri.open_simp_lookup("hydrus_loam") @@ -86,20 +106,149 @@ peri.open_global_lookup(peri.water_filename) #print("Creating the global lookup table took about ", elapsed_time) # -hsr, hsr_base, hsr_simp, hsr_global = peri.soil_root_interface_potentials(rx, sx, inner_kr, rho) +#hsr = peri.soil_root_interface_potentials(rx, sx, inner_kr, rho) +hsr = np.array([peri.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], peri.sp) for i in range(0, len(rx))]) tests.loc[:,"hsr"] = hsr +print("norm of the root soil matrix potentials: ", LA.norm(hsr)) #the standard implementation -tests.loc[:,"hsr_base"] = hsr_base +hsr1= peri.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) +tests.loc[:,"hsr_base"] = hsr1 +print("Norm of the difference to basic lookup table:", LA.norm(hsr-hsr1)) #the first simplified implementation -tests.loc[:,"hsr_simp"] = hsr_simp +hsr2 = peri.soil_root_interface_potentials_table_simp(rx, sx, inner_kr, rho) +tests.loc[:,"hsr_simp"] = hsr2 +print("Norm of the difference to simp lookup table:", LA.norm(hsr-hsr2)) #the alternative implementation -tests.loc[:,"hsr_global"] = hsr_global +hsr3 = peri.soil_root_interface_potentials_table_global(rx, sx, inner_kr, rho) +tests.loc[:,"hsr_global"] = hsr3 +print("Norm of the difference to global lookup table:", LA.norm(hsr-hsr3)) + +waterflow = 2*3.14*np.multiply((hsr-rx),inner_kr) +Phi_root = hsr +Phi_soil = np.array([vg.fast_mfp[sp](sx[i]) for i in range(len(sx))]) + +#base solutes +tests.loc[:,"sol_c"] = c_sol +tests.loc[:,"sol_U"] = np.array([Vmax[i]*c_sol[i]/(Km[i]+c_sol[i])*1e6 for i in range(len(c_sol))]) + +# steady state solutes +solutes_ss = peri.soil_root_solutes_ss_(Phi_root, Phi_soil, c_sol, Vmax, Km, Ds, waterflow, peri.sp) +tests.loc[:,"sol_c_ss"] = solutes_ss +tests.loc[:,"sol_U_ss"] = np.array([Vmax[i]*solutes_ss[i]/(Km[i]+solutes_ss[i])*1e6 for i in range(len(solutes_ss))]) + +# steady rate solutes +#solutes_sr = peri.soil_root_solutes_sr_(Phi_root, Phi_soil, rho, c_sol, Vmax, Km, Ds, waterflow, peri.sp) +#tests.loc[:,"sol_c_sr"] = solutes_sr +#tests.loc[:,"sol_U_sr"] = np.array([Vmax[i]*solutes_sr[i]/(Km[i]+solutes_sr[i])*1e6 for i in range(len(solutes_sr))]) + +# generate the values for the solute (and water?) uptake with dumux rosi: + +#print(tests) + +# for i in range(ntests): + # cyl = RichardsWrapper(RichardsNCCylFoam()) + + # points = [0.03,0.03*tests["rho"].iloc[0]] # radius of discretisation + # NC = 100 # number of discretisations + 1 + # seg_length = 1 + # cyl.createGrid1d(points)# cm + # cyl.setParameter( "Soil.Grid.Cells",str( NC-1)) + # cyl.seg_length = seg_length + # cyl.setTopBC("noFlux") + # cyl.setBotBC("constantPressure") + # cyl.setTopBC_solute("noFlux") + # cyl.setBotBC_solute("michaelisMenten") + # cyl.setParameter("RootSystem.Uptake.Vmax",tests.loc[str(i),"Vmax"]) + # cyl.setParameter("RootSystem.Uptake.Km",tests.loc[str(i),"Km"]) + # Cells = cyl.getCellCenters_().reshape(-1) + # CellsStr = cyl.dumux_str(Cells/100)#cm -> m #changed by Erik + # cyl.setParameter("Soil.IC.Z",CellsStr)# m + # cyl.setParameter("Soil.IC.C"+str(j)+"Z",CellsStr) # m + # cyl.setCriticalPressure(-15000) # cm pressure head + +print("All inputs:") +print(tests[inputs]) +print("All outputs:") +print(tests[outputs]) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# #print("solute tests") + +# #peri.create_integralDiffusion_lookup("hydrus_loam_solutes", peri.sp) +# peri.open_lookup_solutes("hydrus_loam_solutes") + +# labels_sol = ["Phi_root", "Phi_soil", "c_bulk", "Vmax", "Km", "Ds", "waterflow", "c_rootsoil"] + +# Intervals_sol = { + # "Phi_root": [0.0,50], + # "Phi_soil": [60,140], + # "c_bulk": [1.5e-8,1.5e-7], + # "Vmax": [3.5e-6,3.6e-6], + # "Km": [1.4e-7,1.5e-7], + # "waterflow": [0.0,0.0], + # } +# Intervals_sol = pd.DataFrame(Intervals_sol, index = ["min","max"]) + +# tests_sol = pd.DataFrame(index = range(ntests),columns = labels_sol) + +# for one_label in Intervals_sol.columns: + # min_val = Intervals_sol.loc["min",one_label] + # max_val = Intervals_sol.loc["max",one_label] + + # testvalues = (max_val - min_val) * np.random.rand(ntests) + min_val * np.ones(ntests) + # tests_sol[one_label] = testvalues + +# Phi_root = np.array(tests_sol.loc[:,"Phi_root"]) +# Phi_soil = np.array(tests_sol.loc[:,"Phi_soil"]) +# c_bulk = np.array(tests_sol.loc[:,"c_bulk"]) +# Vmax = np.array(tests_sol.loc[:,"Vmax"]) +# Km = np.array(tests_sol.loc[:,"Km"]) +# waterflow = np.array([(2*3.14*inner_kr[i]*(hsr[i]-rx[i])) for i in range(ntests)]) +# Ds = 1.9e-5 + +# #c_rootsoil = peri.soil_root_solutes(Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow) + +# tests_sol.loc[:,"c_rootsoil"] = c_rootsoil -print(tests) +# #print(tests_sol) From 352cee82a49bda8034472b913bca4281393e5221 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Fri, 17 Apr 2026 16:02:15 +0200 Subject: [PATCH 06/41] fix steady state solute uptake --- src/functional/Perirhizal.py | 4 ++-- test/test_perirhizal.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index f903b42b5..e0a090606 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -228,10 +228,10 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf for i in range(n_segments): F_tilde[i]=math.exp(D_tilde*F[i]) a1=c_bulk[i]/F_tilde[i] - a2=(1-F_tilde[i])/(F_tilde[i]*waterflow[i]) + a2=(F_tilde[i]-1)/(F_tilde[i]*waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 - rsc[i]=math.sqrt(pow(p/2,2)-q)-p/2 + rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) return rsc diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 083331203..dbcd8abe6 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -39,6 +39,7 @@ inputs = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km"] outputs = ["hsr","hsr_base","hsr_simp","hsr_global","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] +outputs = ["rx","sx","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] #kr is assumed to lie between .5 and 5e-6 cm/hPa/d = 0.5 to 5e-6 1/d #r is assumed to be 0.03 @@ -76,7 +77,8 @@ c_sol = np.array(tests.loc[:,"c_sol"]) Vmax = np.array(tests.loc[:,"Vmax"]) Km = np.array(tests.loc[:,"Km"]) -Ds = 1.0e2 +Ds = 1.0e1 +r_root = 0.03 hydrus_loam = [0.078, 0.43, 0.036, 1.56, 24.96] sp = vg.Parameters(hydrus_loam) @@ -92,7 +94,7 @@ #create the small lookup table #t = time.time() -peri.create_lookup_simp("hydrus_loam", sp) +#peri.create_lookup_simp("hydrus_loam", sp) #elapsed_time = time.time() - t peri.open_simp_lookup("hydrus_loam") @@ -108,6 +110,7 @@ #hsr = peri.soil_root_interface_potentials(rx, sx, inner_kr, rho) +#numerically solve each root segment, this is the reference solution hsr = np.array([peri.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], peri.sp) for i in range(0, len(rx))]) tests.loc[:,"hsr"] = hsr print("norm of the root soil matrix potentials: ", LA.norm(hsr)) @@ -127,7 +130,8 @@ tests.loc[:,"hsr_global"] = hsr3 print("Norm of the difference to global lookup table:", LA.norm(hsr-hsr3)) -waterflow = 2*3.14*np.multiply((hsr-rx),inner_kr) +#waterflow = 2*3.14*np.multiply((hsr-rx),inner_kr)/r_root +waterflow = -2*3.14*np.multiply((sx-rx),inner_kr)#/r_root Phi_root = hsr Phi_soil = np.array([vg.fast_mfp[sp](sx[i]) for i in range(len(sx))]) From 8381e6595e720c1017567e0bd771c0515527bcbc Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sat, 25 Apr 2026 08:59:33 +0200 Subject: [PATCH 07/41] solute implementation works --- .gitignore | 2 - src/functional/Perirhizal.py | 318 +++++++++++++++----------------- src/functional/van_genuchten.py | 8 +- test/test_perirhizal.py | 47 ++--- 4 files changed, 175 insertions(+), 200 deletions(-) diff --git a/.gitignore b/.gitignore index 527ec9725..4ae2a46b4 100644 --- a/.gitignore +++ b/.gitignore @@ -57,5 +57,3 @@ tutorial/jupyter/.ipynb_checkpoints/ *.png .vscode/settings.json -# lookup tables -*.npz diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index e0a090606..2a7bdd41a 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -21,11 +21,11 @@ class PerirhizalPython(Perirhizal): * calculates root soil interface potential using steady rate approximation (Schröder et al. 2008) * perirhizal_conductance_per_layer calculates the perirhizal conductance in a soil layer (or cell) (Vanderborght et al. 2023, Eqn [6]) - * support of 4D lookup tables (file type is a zipped archive) + * support of 2D lookup tables (file type is a zipped archive) * analysis across soil grid, e.g. get_density, average, aggregate * calculates outer perirhizal radii (based on denisties or Voronoi) - run script to create a 4D lookup table (see __main__) + run script to create a 2D lookup table (see __main__) """ def __init__(self, ms=None): @@ -35,39 +35,33 @@ def __init__(self, ms=None): else: super().__init__() - self.lookup_table = None # optional 4d look up table to find soil root interface potentials - self.simp_lookup_table = None # optional 2d look up table to find soil root interface potentials + self.lookup_table = None # optional 2d look up table to find soil root interface potentials self.global_lookup_table = None # optional 3d look up table to find soil root interface potentials - self.lookup_table_solutes = None - self.sp = None # corresponding van gencuchten soil parameter + self.lookup_table_solutes = None # optional 1d look up table for steady state solute flow + self.lookup_table_sr_solutes = None # optional 2d lookup tables for the steady rate solute flow (one every diffusion coefficient) + self.sp = None # corresponding van genuchten soil parameter self.alpha_0 = 0.3 # a constant that is used for numerically solving the perirhizal waterflow self.h_wilt = -16000 - self.water_filename = "lookup_perirhizal_waterflow_global" #name for the global lookup table + self.water_filename = "lookup_perirhizal_waterflow_global" #name and location for the global lookup table def set_soil(self, sp): """sets VG parameters, and no look up table (slow)""" vg.create_mfp_lookup(sp) self.sp = sp self.lookup_table = None - self.simp_lookup_table = None - + self.global_lookup_table = None + self.lookup_table_solutes = None + self.lookup_table_sr_solutes = None + + def open_lookup(self, filename): - """ opens a look-up table from a file to quickly find soil root interface potentials """ + """opens a look-up table from a file to quickly find soil root interface potentials""" npzfile = np.load(filename + ".npz") interface = npzfile["interface"] - rx_, sx_, akr_, rho_ = npzfile["rx_"], npzfile["sx_"], npzfile["akr_"], npzfile["rho_"] - soil = npzfile["soil"] - self.lookup_table = RegularGridInterpolator((rx_, sx_, akr_, rho_), interface) # method = "nearest" fill_value = None , bounds_error=False - self.sp = vg.Parameters(soil) - - def open_simp_lookup(self, filename): - """opens a smaller look-up table from a file to quickly find soil root interface potentials""" - npzfile = np.load(filename + "_simp.npz") - interface = npzfile["interface"] inner_kr_b, base_mfp = npzfile["inner_kr_b_"], npzfile["base_mfp_"] soil = npzfile["soil"] - self.simp_lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp), interface) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_table = RegularGridInterpolator((inner_kr_b, base_mfp), interface) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) vg.create_mfp_lookup(self.sp) # I do not know why this has to be repeated here @@ -84,13 +78,24 @@ def open_lookup_solutes(self, filename): """opens a look-up table from a file to quickly find soil root solute concentrations in the steady state case""" npzfile = np.load(filename + ".npz") F_integral = npzfile["F_integral"] - base_mfp_ = npzfile["base_mfp_"] + mfp_ = npzfile["mfp_"] soil = npzfile["soil"] - self.lookup_table_sssolutes = RegularGridInterpolator((base_mfp_), F_integral) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_table_solutes = RegularGridInterpolator((mfp_), F_integral) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) - vg.create_mfp_lookup(self.sp) # I do not know why this has to be repeated here + vg.create_mfp_lookup(self.sp) # does this have to be repeated here? + + def open_lookup_sr_solutes(self, filename): + """opens an additional look-up table from a file to quickly find soil root solute concentrations in the steady rate case""" + npzfile = np.load(filename + ".npz") + self.open_lookup_solutes(filename) #repeat steady state case + R_sr = npzfile["R_sr"] + D_s_, outer_mfp_, inner_mfp_ = npzfile["D_s_"], npzfile["outer_mfp_"], npzfile["inner_mfp_"] + soil = npzfile["soil"] + self.lookup_table_sr_solutes = RegularGridInterpolator((D_s, outer_mfp_, inner_mfp_), R_sr) # method = "nearest" fill_value = None , bounds_error=False + self.sp = vg.Parameters(soil) + vg.create_mfp_lookup(self.sp) # does this have to be repeated here? - def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): #TODO: change this after the debugging is done + def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): """ finds matric potentials at the soil root interface for as all segments uses a look up tables if present (see create_lookup, and open_lookup) @@ -106,13 +111,10 @@ def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): #TODO: change t if self.lookup_table: rsx = self.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) else: - if self.simp_lookup_table: - rsx = self.soil_root_interface_potentials_table_simp(rx, sx, inner_kr, rho) + if self.global_lookup_table: + rsx = self.soil_root_interface_potentials_table_global(rx, sx, inner_kr, rho) else: - if self.global_lookup_table: - rsx = self.soil_root_interface_potentials_table_global(rx, sx, inner_kr, rho) - else: - rsx = np.array([PerirhizalPython.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], self.sp) for i in range(0, len(rx))]) + rsx = np.array([PerirhizalPython.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], self.sp) for i in range(0, len(rx))]) return rsx @@ -146,6 +148,7 @@ def soil_root_interface_(rx, sx, inner_kr, rho, sp): def soil_root_interface_simp_(self, inner_kr_b, base_mfp, sp): """ finds matric potential at the soil root interface for as single segment + gives (numerically) the same "results as soil_root_interface_", but is much simpler to create a lookup table for rx xylem matric potential [cm] sx bulk soil matric potential [cm] @@ -236,20 +239,22 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf return rsc - def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, waterflow, sp): + def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, waterflow, sp, approximate_rho = True): """ - finds solute concentration at the soil root interface for all segments assuming steady r - uses a look up tables if present (see create_lookup, and open_lookup) + finds solute concentration at the soil root interface for all segments assuming steady rate + largely reuses approach of the steady state case, it just needs to compute the factor between mean concentration and the concentration at the outer boundary + uses a look up tables if present (see create_lookup, and open_lookup) #TODO cange names Phi_root matrix flux potential at the root-soil-interface [cm2/d] Phi_soil matrix flux potential at the bulk soil [cm2/d] - rho geometry factor [1] (outer_radius / inner_radius) + rho geometry factor (outer_radius / inner_radius) [1] c_bulk solute concentration at the bulk soil [mol/cm3] Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] waterflow steady state waterflow (entire root circumference) [cm2/d] sp van Genuchten parameter set + approximate_rho set rho = 0.01 in order to use a small lookup table """ assert len(Phi_root) == len(Phi_soil) == len(rho) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" @@ -259,37 +264,45 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w F = np.zeros(n_segments) #F is a helper values F_tilde = np.zeros(n_segments) R_sr = np.zeros(n_segments) + watercontent = np.zeros(n_segments) + #repeat from steady state if self.lookup_table_solutes: F=[(self.lookup_table_solutes(Phi_soil[i])-self.lookup_table_solutes(Phi_root[i])) for i in range(0, len(c_bulk))] else: F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] - - #compute a prefactor - D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) - C_d = self.alpha_0*self.sp.Ksat/sp.alpha*D_tilde - + C_d = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) + if approximate_rho: + if self.lookup_table_sr_solutes: + R_sr=[self.lookup_table_sr_solutes(C_d, Phi_root[i], Phi_soil[i]) for i in range(0, len(c_bulk))] + else: + R_sr=[self.integral_overconcentration_(C_d, Phi_root[i], Phi_soil[i], 100.0, self.sp) for i in range(0, len(c_bulk))] + else: + R_sr=[self.integral_overconcentration_(C_d, Phi_root[i], Phi_soil[i], rho[i], self.sp) for i in range(0, len(c_bulk))] + + watercontent = vg.water_content(vg.fast_imfp[sp](Phi_soil), self.sp) #mean watercontent of the perirhizal zone + print("R_sr", R_sr) + #reuse steady state case #solve quadratic eqation # TODO: Link to publication for i in range(n_segments): - R_sr[i] = self.integral_overconcentration_(Phi_root[i], Phi_soil[i], rho[i], C_d, self.sp) - F_tilde[i]=math.exp(D_tilde*F[i]) - a1=c_bulk[i]/(F_tilde[i]*R_sr[i]) #*vg.water_content(vg.fast_imfp[sp](Phi_soil), self.sp) - a2=(1-F_tilde[i]*R_sr[i])/(F_tilde[i]*R_sr[i]*waterflow[i]) + F_tilde[i]=math.exp(C_d*F[i]) + a1=(c_bulk[i]*watercontent[i])/(F_tilde[i]*R_sr[i]) + a2=(F_tilde[i]*R_sr[i]-watercontent[i])/(F_tilde[i]*R_sr[i]*waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 - rsc[i]=math.sqrt(pow(p/2,2)-q)-p/2 - + rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) + return rsc @staticmethod def integral_overDiffusion_(Phi, sp): """ - Integral of (D_s(theta_s-theta_r)^(13/3))/(theta*D(theta)) from Phi = 0 to Phi + Integral of (D_s(theta_s-theta_r)^(13/3))/(theta*D(theta)) from Phi = 0.001 to Phi Phi matrix flux potential [cm2/d] sp soil parameter: van Genuchten parameter set (type vg.Parameters) @@ -298,38 +311,60 @@ def integral_overDiffusion_(Phi, sp): return 0 theta_rel = sp.theta_R/(sp.theta_S-sp.theta_R) integral_fun = lambda Phi: pow(theta_rel+vg.effective_saturation(vg.fast_imfp[sp](Phi),sp),-13/3) - integral_overD, _ = integrate.quad(integral_fun, 0, Phi) + integral_overD, _ = integrate.quad(integral_fun, 1.0e-3, Phi) return integral_overD - def integral_overconcentration_(self, Phi_in, Phi_out, rho, C_d, sp): + def integral_overconcentration_(self, Ds, Phi_root, Phi_soil, rho, sp): """ (2 pi * 0.9999 rprhiz^2 (c_rs-C_SW))/(Integral from 0.01*rprhiz to rprhiz of 2 pi s (c(s)-C_SW) ds) - Phi_in matrix flux potential at 0.01 r_prhiz [cm2/d] - Phi_out matrix flux potential at r_prhiz [cm2/d] - rho r_prhiz / r_root - C_d (alpha_0 K_s / alpha) (theta_s^2/(D_s (theta_s-theta_r)^(13/3))) + Ds Diffusion coefficient + Phi_root, Phi_soil matrix flux potential at the root and soil [cm2/d] + rho r_prhiz / r_root - Assume Phi(s=r/r-prhiz)=As^2-2Aln(s)+C (steady rate) - A=(Phi_in-Phi_out)/(0.01^2-2*ln(0.01)-1) - C=Phi_out-A - """ + """ - A = (Phi_in-Phi_out)/(1/(rho*rho)-np.log(1/(rho*rho))-1) - C = Phi_out-A + Phi_A, Phi_C = self.determine_mfp_function(Phi_root, Phi_soil, rho) + C_d = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) if self.lookup_table_solutes: - F0=self.lookup_table_solutes(A*(1/(rho*rho)-np.log(1/(rho*rho)))+C) - integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](A*(s2-np.log(s2))+C), self.sp)*math.exp(C_d*(self.lookup_table_solutes(A*(s2-np.log(s2))+C)-F0)) + #F0=self.lookup_table_solutes(Phi_A*(1/(rho*rho)-np.log(1/(rho*rho)))+Phi_C) + #F0=self.lookup_table_solutes(Phi_A*(0.53**2-2*np.log(0.53))+Phi_C) + F0=self.lookup_table_solutes(Phi_A*(1.0**2-2*np.log(1.0))+Phi_C) + integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](Phi_A*(s2-np.log(s2))+Phi_C), self.sp)*math.exp(C_d*(self.lookup_table_solutes(Phi_A*(s2-np.log(s2))+Phi_C)-F0)) else: - F0=self.integral_overDiffusion_(A*(1/(rho*rho)-np.log(1/(rho*rho)))+C, self.sp) - integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](A*(s2-np.log(s2))+C), self.sp)*math.exp(C_d*(self.integral_overDiffusion_(A*(s2-np.log(s2))+C, self.sp)-F0)) - R_sr, _ = integrate.quad(integral_fun, 1/(rho*rho), 1.0) - + #F0=self.integral_overDiffusion_(Phi_A*(1/(rho*rho)-np.log(1/(rho*rho)))+Phi_C, self.sp) + #F0=self.integral_overDiffusion_(Phi_A*(0.53**2-2*np.log(0.53))+Phi_C, self.sp) + F0=self.integral_overDiffusion_(Phi_A*(1.0**2-2*np.log(1.0))+Phi_C, self.sp) + integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](Phi_A*(s2-np.log(s2))+Phi_C), self.sp)*math.exp(C_d*(self.integral_overDiffusion_(Phi_A*(s2-np.log(s2))+Phi_C, self.sp)-F0)) + integral_R_sr, _ = integrate.quad(integral_fun, 1/(rho*rho), 1.0**2) + R_sr = integral_R_sr / ((1.0**2-1/(rho*rho))) + print(integral_fun(1/(rho*rho)),integral_fun(0.53**2),integral_fun(1.0**2),integral_R_sr, rho) return R_sr - + + def determine_mfp_function(self, Phi_root, Phi_soil, rho): + """ + determine the spatial function of the mfp + + Phi(r/r_prhiz)= A(s^2-ln(s^2))+C + + A (0.53^2-ln(0.53^2))+C=Phi_soil + A (0.01^2-ln(0.01^2))+C=Phi_root + + A = (Phi_soil-Phi_root)/(0.53^2-ln(0.53^2)-0.01^2+ln(0.01^2) + C = ((0.53^2-ln(0.53^2)) * Phi_root - (0.01^2-ln(0.01^2)) * Phi_soil)/(0.53^2-ln(0.53^2)-0.01^2+ln(0.01^2) + """ + + det = 0.53**2-2*math.log(0.53)-(1/rho)**2+2*math.log(1/rho) + a = 0.53**2-2*math.log(0.53) + c = (1/rho)**2-2*math.log(1/rho) + + Phi_A = min((Phi_soil-Phi_root) / det, 0) + Phi_C = max((a*Phi_root-c*Phi_soil) / det + c * Phi_A, 1.0e-6) - c * Phi_A + + return Phi_A, Phi_C def perirhizal_conductance_per_layer(self, h_bs, h_sr, sp): """ @@ -352,8 +387,11 @@ def perirhizal_conductance_per_layer(self, h_bs, h_sr, sp): l_root = rld * dz # [cm-1] return 2 * np.pi * np.multiply(l_root, np.multiply(b, k_prhiz(h_bs, h_sr))) # [day-1], see Vanderborght et al. (2023), Eqn [6] + # depricated, leave the function in in case we ever want to compute big lookup tables again def create_lookup_mpi(self, filename, sp): """ + depricated + Precomputes all soil root interface potentials for a specific soil type and saves results into a 4D lookup table @@ -439,38 +477,6 @@ def create_lookup_mpi(self, filename, sp): self.sp = sp def create_lookup(self, filename, sp): - """ - Precomputes all soil root interface potentials for a specific soil type - and saves results into a 4D lookup table - - filename three files are written (filename, filename_, and filename_soil) - sp van genuchten soil parameters, , call - vg.create_mfp_lookup(sp) before - """ - rxn = 150 - rx_ = -np.logspace(np.log10(1.), np.log10(16000), rxn) - rx_ = rx_ + np.ones((rxn,)) - rx_ = rx_[::-1] - sxn = 150 - sx_ = -np.logspace(np.log10(1.), np.log10(16000), sxn) - sx_ = sx_ + np.ones((sxn,)) - sx_ = sx_[::-1] - akrn = 100 - akr_ = np.logspace(np.log10(1.e-7), np.log10(1.e-4), akrn) - rhon = 30 - rho_ = np.logspace(np.log10(2.), np.log10(200.), rhon) # TODO: talk about the lower bound here: rho = 1 leads to problems - interface = np.zeros((rxn, sxn, akrn, rhon)) - for i, rx in enumerate(rx_): - print(i) - for j, sx in enumerate(sx_): - for k, akr in enumerate(akr_): - for l, rho in enumerate(rho_): - interface[i, j, k, l] = PerirhizalPython.soil_root_interface_(rx, sx, akr, rho, sp) - np.savez(filename, interface = interface, rx_ = rx_, sx_ = sx_, akr_ = akr_, rho_ = rho_, soil = list(sp)) - self.lookup_table = RegularGridInterpolator((rx_, sx_, akr_, rho_), interface) - self.sp = sp - - def create_lookup_simp(self, filename, sp): """ Precomputes all soil root interface potentials for a specific soil type and saves results into a 2D lookup table @@ -511,12 +517,14 @@ def create_lookup_simp(self, filename, sp): interface = np.zeros((inner_kr_bn,base_mfp_n)) + print("Creating a lookup table for the hydraulic perirhizal resistance model") + for i, inner_kr_b in enumerate(inner_kr_b_): - print(i) + print(i, " / ", inner_kr_bn) for j, base_mfp in enumerate(base_mfp_): interface[i, j] = PerirhizalPython.soil_root_interface_simp_(self, inner_kr_b, base_mfp, sp) - np.savez(filename+"_simp", interface=interface, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, soil=list(sp)) - self.simp_lookup_table = RegularGridInterpolator((inner_kr_b_, base_mfp_), interface) + np.savez(filename, interface=interface, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, soil=list(sp)) + self.lookup_table = RegularGridInterpolator((inner_kr_b_, base_mfp_), interface) self.sp = sp def create_lookup_global(self, filename, sp): @@ -531,11 +539,11 @@ def create_lookup_global(self, filename, sp): # intervals for all inputs - rx_int_abs = [1.0, 16000.0] #absolute values for logarithmic scaling - sx_int_abs = [1.0, 16000.0] + rx_int_abs = [0.1, 16000.0] #absolute values for logarithmic scaling + sx_int_abs = [0.09, 16000.0] akr_int = [1.0e-7,1.0e-4] rho_int = [2.0,200.0] - vg_m_int = [0.1,0.8] + vg_m_int = [0.1,1.5] vg_Ks_int = [1.0,100.0] vg_alpha_int = [0.01,0.3] @@ -554,10 +562,11 @@ def create_lookup_global(self, filename, sp): interface_vg = np.zeros((vg_m_n,sx_n)) #construct a smaller lookup table for the simplified matrix flux potential + print("Creating a global lookup table for the matrix flux potential") for i, vg_m in enumerate(vg_m_): - print(i) + print(i, " / ", vg_m_n) sp_dummy = vg.Parameters([0.1,0.4,self.alpha_0,1./(1.-vg_m),1.0]) - vg.create_mfp_lookup(sp_dummy) + vg.create_mfp_lookup(sp_dummy, verbose=False) for j, sx in enumerate(sx_): interface_vg[i, j] = vg.matric_flux_potential(sx, sp_dummy) self.lookup_global_mfp = RegularGridInterpolator((vg_m_, sx_), interface_vg) @@ -581,9 +590,9 @@ def create_lookup_global(self, filename, sp): tol = 0.001 #tolerance for extreme values of the matrix flux potential at the soil -> output h_rs = h_soil or h_x - + print("Creating a global lookup table for the hydraulic perirhizal resistance model") for i, vg_m in enumerate(vg_m_): - print(i) + print(i, " / ", vg_m_n) for j, inner_kr_b in enumerate(inner_kr_b_): for k, base_mfp in enumerate(base_mfp_): interface[i, j, k] = PerirhizalPython.soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m) @@ -602,70 +611,47 @@ def create_integralDiffusion_lookup(self, filename, sp): Phin = 300 Phi_ = np.logspace(np.log10(1.0e-6), np.log10(300), Phin) base_mfp_=Phi_ - integral_overD = np.zeros((Phin,2)) + integral_overD = np.zeros((Phin)) for i, Phi in enumerate(Phi_): print(i) - integral_overD[i,0] = PerirhizalPython.integral_overDiffusion_(Phi, sp) - integral_overD[i,1] = integral_overD[i,0] + integral_overD[i] = PerirhizalPython.integral_overDiffusion_(Phi, sp) np.savez(filename, integral_overD = integral_overD, base_mfp_ = base_mfp_, soil = list(sp)) self.lookup_table_solutes = RegularGridInterpolator((base_mfp_, np.array([0,1])) , integral_overD) self.sp = sp + return integral_overD, base_mfp_ + + def create_integralconcentration_lookup(self, filename, Ds_, sp): + """ + Precomputes all integrals for the steady state solute flow - - def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): - """ - finds potential at the soil root interface using a lookup table - - rx xylem matric potential [cm] - sx bulk soil matric potential [cm] - inner_kr root radius times hydraulic conductivity [cm/day] - rho geometry factor [1] - f function to look up the potentials + Phi upper matrix flux potential (bottom is set to 0) [cm2/d] + Ds_ array of all diffusion coefficients [cm2/d] + sp van genuchten soil parameters, , call + vg.create_mfp_lookup(sp) before """ - try: - sx = np.array(sx) - mask = inner_kr_ == 0 - inner_kr_[mask] = 1.0e-7 - rsx = self.lookup_table((rx, sx, inner_kr_, rho_)) - rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential - except: - if np.max(rx) > 0: - print("xylem matric potential positive", np.max(rx), "at", np.argmax(rx)) - if np.min(rx) < -16000: - print("xylem matric potential under -16000 cm", np.min(rx), "at", np.argmin(rx)) - if np.max(sx) > 0: - print("soil matric potential positive", np.max(sx), "at", np.argmax(sx)) - if np.min(sx) < -16000: - print("soil matric potential under -16000 cm", np.min(sx), "at", np.argmin(sx)) - if np.min(inner_kr_) < 1.0e-7: - print("radius times radial conductivity below 1.e-7", np.min(inner_kr_), "at", np.argmin(inner_kr_)) - if np.max(inner_kr_) > 1.0e-4: - print("radius times radial conductivity above 1.e-4", np.max(inner_kr_), "at", np.argmax(inner_kr_)) - if np.min(rho_) < 1: - print("geometry factor below 1", np.min(rho_), "at", np.argmin(rho_)) - if np.max(rho_) > 200: - print("geometry factor above 200", np.max(rho_), "at", np.argmax(rho_)) - - print("PerirhizalPython.soil_root_interface_potentials_table(): table look up failed, value exceeds table") - print( - "\trx", - np.min(rx), - np.max(rx), - "sx", - np.min(sx), - np.max(sx), - "inner_kr", - np.min(inner_kr_), - np.max(inner_kr_), - "rho", - np.min(rho_), - np.max(rho_), - ) - raise - - return rsx - - def soil_root_interface_potentials_table_simp(self, rx, sx, inner_kr_, rho_): + + #repeat steady state case + integral_overD, base_mfp_ = self.create_integralDiffusion_lookup(filename, self.sp) + + n_Ds = len(Ds_) + + Phin = 300 + Phi_ = np.logspace(np.log10(1.0e-6), np.log10(300), Phin) + outer_mfp_=Phi_ + inner_mfp_=Phi_ + R_sr_ = np.zeros((n_Ds,Phin,Phin)) + for i, Ds in enumerate(Ds_): + print("starting with diffusion coefficient number ", str(i+1), ":") + for j, Phi_in in enumerate(inner_mfp_): + print(j) + for k, Phi_out in enumerate(outer_mfp_): + R_sr_[i,j,k] = PerirhizalPython.integral_overconcentration_(Ds, Phi, sp) + + np.savez(filename, integral_overD=integral_overD, base_mfp_=base_mfp_, D_s_ = D_s_, outer_mfp_ = outer_mfp_, inner_mfp_ = inner_mfp_, R_sr_ = R_sr_, soil = list(sp)) + self.lookup_table_sr_solutes = RegularGridInterpolator((base_mfp_, np.array([0,1])) , integral_overD) + self.sp = sp + + def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): """ finds potential at the soil root interface using a lookup table @@ -701,7 +687,7 @@ def soil_root_interface_potentials_table_simp(self, rx, sx, inner_kr_, rho_): inner_kr_b = np.divide(inner_kr_,b) base_mfp = - (inner_kr_b * rx + vg.fast_mfp[self.sp](sx)) - rsx = self.simp_lookup_table((inner_kr_b, base_mfp)) + rsx = self.lookup_table((inner_kr_b, base_mfp)) rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential except: if np.max(rx) > 0: @@ -1232,7 +1218,7 @@ def get_default_volumes_(self): vg.create_mfp_lookup(sp) peri = PerirhizalPython() #peri.create_lookup_mpi(filename, sp) # takes some hours; mpiexec -n 4 python Perirhizal.py - peri.create_lookup_simp(filename, sp) # should be better + peri.create_lookup(filename, sp) # should be better peri.create_lookup_global(peri.water_filename) # this is global # generate some tests diff --git a/src/functional/van_genuchten.py b/src/functional/van_genuchten.py index 61a193278..34b60f675 100644 --- a/src/functional/van_genuchten.py +++ b/src/functional/van_genuchten.py @@ -154,9 +154,10 @@ def matric_potential_mfp(mfp, sp): call create_mfp_lookup first, once for each soil parameter @param sp""" -def create_mfp_lookup(sp, wilting_point = -16000, n = 16001): +def create_mfp_lookup(sp, wilting_point = -16000, n = 16001, verbose = True): """ initializes the look up tables for soil parameter to use fast_mfp, and fast_imfp """ - print("initializing look up tables") + if verbose: + print("initializing mfp look up tables") global fast_mfp global fast_imfp @@ -176,7 +177,8 @@ def create_mfp_lookup(sp, wilting_point = -16000, n = 16001): imfp[i] = h_[i] fast_imfp[sp] = interpolate.interp1d(mfp, imfp, bounds_error = False, fill_value = (imfp[0], imfp[-1])) # - print("done") + if verbose: + print("done") # fast_specific_moisture_storage = {} # fast_water_content = {} diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index dbcd8abe6..0a7beafac 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -33,13 +33,11 @@ sp_alpha sp_Ksat """ -labels = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km","hsr","hsr_base","hsr_simp","hsr_global","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] -#for now only water flow: -#labels = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km","hsr","hsr_1d","hsr_base","hsr_simp","hsr_global"] +labels = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km","hsr","hsr_lookup","hsr_global","waterflow","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] inputs = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km"] -outputs = ["hsr","hsr_base","hsr_simp","hsr_global","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] -outputs = ["rx","sx","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] +outputs = ["hsr","hsr_lookup","hsr_global","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] +outputs = ["rx","sx","waterflow","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] #kr is assumed to lie between .5 and 5e-6 cm/hPa/d = 0.5 to 5e-6 1/d #r is assumed to be 0.03 @@ -56,7 +54,7 @@ } Intervals = pd.DataFrame(Intervals, index = ["min","max"]) -ntests = 30 +ntests = 10 tests = pd.DataFrame(index = range(ntests),columns = labels) for one_label in Intervals.columns: @@ -86,28 +84,22 @@ #create the big lookup table #t = time.time() -#peri.create_lookup_mpi("hydrus_loam", sp) +#peri.create_lookup("results/hydrus_loam", sp) #elapsed_time = time.time() - t #print("Creating the big lookup table took about ", elapsed_time) # -peri.open_lookup("hydrus_loam") +peri.open_lookup("results/hydrus_loam") -#create the small lookup table -#t = time.time() -#peri.create_lookup_simp("hydrus_loam", sp) -#elapsed_time = time.time() - t - -peri.open_simp_lookup("hydrus_loam") -#print("Creating the simp lookup table took about ", elapsed_time) # #create the global lookup table #t = time.time() -#peri.create_lookup_global(peri.water_filename, sp) +#peri.create_lookup_global("results/"+peri.water_filename, sp) #elapsed_time = time.time() - t - -peri.open_global_lookup(peri.water_filename) #print("Creating the global lookup table took about ", elapsed_time) # +peri.open_global_lookup("results/"+peri.water_filename) + + #hsr = peri.soil_root_interface_potentials(rx, sx, inner_kr, rho) #numerically solve each root segment, this is the reference solution @@ -117,23 +109,19 @@ #the standard implementation hsr1= peri.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) -tests.loc[:,"hsr_base"] = hsr1 +tests.loc[:,"hsr_lookup"] = hsr1 print("Norm of the difference to basic lookup table:", LA.norm(hsr-hsr1)) -#the first simplified implementation -hsr2 = peri.soil_root_interface_potentials_table_simp(rx, sx, inner_kr, rho) -tests.loc[:,"hsr_simp"] = hsr2 -print("Norm of the difference to simp lookup table:", LA.norm(hsr-hsr2)) - #the alternative implementation hsr3 = peri.soil_root_interface_potentials_table_global(rx, sx, inner_kr, rho) tests.loc[:,"hsr_global"] = hsr3 print("Norm of the difference to global lookup table:", LA.norm(hsr-hsr3)) #waterflow = 2*3.14*np.multiply((hsr-rx),inner_kr)/r_root -waterflow = -2*3.14*np.multiply((sx-rx),inner_kr)#/r_root -Phi_root = hsr +waterflow = 2*3.14*np.multiply((sx-rx),inner_kr)#/r_root +Phi_root = np.array([vg.fast_mfp[sp](hsr[i]) for i in range(len(hsr))]) Phi_soil = np.array([vg.fast_mfp[sp](sx[i]) for i in range(len(sx))]) +tests.loc[:,"waterflow"] = waterflow #base solutes tests.loc[:,"sol_c"] = c_sol @@ -145,9 +133,10 @@ tests.loc[:,"sol_U_ss"] = np.array([Vmax[i]*solutes_ss[i]/(Km[i]+solutes_ss[i])*1e6 for i in range(len(solutes_ss))]) # steady rate solutes -#solutes_sr = peri.soil_root_solutes_sr_(Phi_root, Phi_soil, rho, c_sol, Vmax, Km, Ds, waterflow, peri.sp) -#tests.loc[:,"sol_c_sr"] = solutes_sr -#tests.loc[:,"sol_U_sr"] = np.array([Vmax[i]*solutes_sr[i]/(Km[i]+solutes_sr[i])*1e6 for i in range(len(solutes_sr))]) +solutes_sr = peri.soil_root_solutes_sr_(Phi_root, Phi_soil, rho, c_sol, Vmax, Km, Ds, waterflow, peri.sp) +tests.loc[:,"sol_c_sr"] = solutes_sr +tests.loc[:,"sol_U_sr"] = np.array([Vmax[i]*solutes_sr[i]/(Km[i]+solutes_sr[i])*1e6 for i in range(len(solutes_sr))]) + # generate the values for the solute (and water?) uptake with dumux rosi: From 7e819819a6c63d285233937d8edb34bbc21b1c2a Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sat, 25 Apr 2026 22:29:11 +0200 Subject: [PATCH 08/41] fixed tutorial reading parameter file --- src/functional/Perirhizal.py | 59 +++++---- src/functional/PlantHydraulicParameters.py | 2 +- test/test_perirhizal.py | 115 ++---------------- .../example7_3_coupling_fp.py | 2 +- .../example7_3_coupling_fp_mpi.py | 2 +- .../chapter7_coupled/example7_3_lookup.py | 2 +- 6 files changed, 49 insertions(+), 133 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 2a7bdd41a..c1c6d58da 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -77,10 +77,10 @@ def open_global_lookup(self, filename): def open_lookup_solutes(self, filename): """opens a look-up table from a file to quickly find soil root solute concentrations in the steady state case""" npzfile = np.load(filename + ".npz") - F_integral = npzfile["F_integral"] - mfp_ = npzfile["mfp_"] + integral_overD_ = npzfile["integral_overD_"] + base_mfp_ = npzfile["base_mfp_"] soil = npzfile["soil"] - self.lookup_table_solutes = RegularGridInterpolator((mfp_), F_integral) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_table_solutes = RegularGridInterpolator((base_mfp_,[0,1]) , integral_overD_) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) vg.create_mfp_lookup(self.sp) # does this have to be repeated here? @@ -88,10 +88,10 @@ def open_lookup_sr_solutes(self, filename): """opens an additional look-up table from a file to quickly find soil root solute concentrations in the steady rate case""" npzfile = np.load(filename + ".npz") self.open_lookup_solutes(filename) #repeat steady state case - R_sr = npzfile["R_sr"] - D_s_, outer_mfp_, inner_mfp_ = npzfile["D_s_"], npzfile["outer_mfp_"], npzfile["inner_mfp_"] + R_sr_ = npzfile["R_sr_"] + Ds_, outer_mfp_, inner_mfp_ = npzfile["Ds_"], npzfile["outer_mfp_"], npzfile["inner_mfp_"] soil = npzfile["soil"] - self.lookup_table_sr_solutes = RegularGridInterpolator((D_s, outer_mfp_, inner_mfp_), R_sr) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_table_sr_solutes = RegularGridInterpolator((Ds_, inner_mfp_, outer_mfp_) , R_sr_) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) vg.create_mfp_lookup(self.sp) # does this have to be repeated here? @@ -220,7 +220,7 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf F_tilde = np.zeros(n_segments) if self.lookup_table_solutes: - F=[(self.lookup_table_solutes(Phi_soil[i])-self.lookup_table_solutes(Phi_root[i])) for i in range(0, len(c_bulk))] + F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, len(c_bulk))] else: F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] @@ -243,7 +243,7 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w """ finds solute concentration at the soil root interface for all segments assuming steady rate largely reuses approach of the steady state case, it just needs to compute the factor between mean concentration and the concentration at the outer boundary - uses a look up tables if present (see create_lookup, and open_lookup) #TODO cange names + uses a look up tables if present (see create_integralconcentration_lookup, create_integralDiffusion_lookup, and open_lookup_ss_solutes, open_lookup_sr_solutes) Phi_root matrix flux potential at the root-soil-interface [cm2/d] Phi_soil matrix flux potential at the bulk soil [cm2/d] @@ -261,14 +261,14 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w n_segments = len(c_bulk) rsc = np.zeros(n_segments) - F = np.zeros(n_segments) #F is a helper values + F = np.zeros(n_segments) #F is a helper value F_tilde = np.zeros(n_segments) R_sr = np.zeros(n_segments) watercontent = np.zeros(n_segments) #repeat from steady state if self.lookup_table_solutes: - F=[(self.lookup_table_solutes(Phi_soil[i])-self.lookup_table_solutes(Phi_root[i])) for i in range(0, len(c_bulk))] + F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, len(c_bulk))] else: F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] @@ -278,14 +278,13 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w if approximate_rho: if self.lookup_table_sr_solutes: - R_sr=[self.lookup_table_sr_solutes(C_d, Phi_root[i], Phi_soil[i]) for i in range(0, len(c_bulk))] + R_sr=[self.lookup_table_sr_solutes((C_d, Phi_root[i], Phi_soil[i])) for i in range(0, len(c_bulk))] else: R_sr=[self.integral_overconcentration_(C_d, Phi_root[i], Phi_soil[i], 100.0, self.sp) for i in range(0, len(c_bulk))] else: R_sr=[self.integral_overconcentration_(C_d, Phi_root[i], Phi_soil[i], rho[i], self.sp) for i in range(0, len(c_bulk))] watercontent = vg.water_content(vg.fast_imfp[sp](Phi_soil), self.sp) #mean watercontent of the perirhizal zone - print("R_sr", R_sr) #reuse steady state case #solve quadratic eqation # TODO: Link to publication for i in range(n_segments): @@ -332,8 +331,10 @@ def integral_overconcentration_(self, Ds, Phi_root, Phi_soil, rho, sp): if self.lookup_table_solutes: #F0=self.lookup_table_solutes(Phi_A*(1/(rho*rho)-np.log(1/(rho*rho)))+Phi_C) #F0=self.lookup_table_solutes(Phi_A*(0.53**2-2*np.log(0.53))+Phi_C) - F0=self.lookup_table_solutes(Phi_A*(1.0**2-2*np.log(1.0))+Phi_C) - integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](Phi_A*(s2-np.log(s2))+Phi_C), self.sp)*math.exp(C_d*(self.lookup_table_solutes(Phi_A*(s2-np.log(s2))+Phi_C)-F0)) + #print("Phi",Phi_A*(1.0**2-2*np.log(1.0))+Phi_C) + F0=self.lookup_table_solutes((Phi_A*(1.0**2-2*np.log(1.0))+Phi_C,0.0)) + #print("Phi2",Phi_A*(s2-np.log(s2))+Phi_C) + integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](Phi_A*(s2-np.log(s2))+Phi_C), self.sp)*math.exp(C_d*(self.lookup_table_solutes((Phi_A*(s2-np.log(s2))+Phi_C,0))-F0)) else: #F0=self.integral_overDiffusion_(Phi_A*(1/(rho*rho)-np.log(1/(rho*rho)))+Phi_C, self.sp) #F0=self.integral_overDiffusion_(Phi_A*(0.53**2-2*np.log(0.53))+Phi_C, self.sp) @@ -341,7 +342,7 @@ def integral_overconcentration_(self, Ds, Phi_root, Phi_soil, rho, sp): integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](Phi_A*(s2-np.log(s2))+Phi_C), self.sp)*math.exp(C_d*(self.integral_overDiffusion_(Phi_A*(s2-np.log(s2))+Phi_C, self.sp)-F0)) integral_R_sr, _ = integrate.quad(integral_fun, 1/(rho*rho), 1.0**2) R_sr = integral_R_sr / ((1.0**2-1/(rho*rho))) - print(integral_fun(1/(rho*rho)),integral_fun(0.53**2),integral_fun(1.0**2),integral_R_sr, rho) + #print(integral_fun(1/(rho*rho)),integral_fun(0.53**2),integral_fun(1.0**2),integral_R_sr, rho) return R_sr def determine_mfp_function(self, Phi_root, Phi_soil, rho): @@ -609,16 +610,18 @@ def create_integralDiffusion_lookup(self, filename, sp): vg.create_mfp_lookup(sp) before """ Phin = 300 - Phi_ = np.logspace(np.log10(1.0e-6), np.log10(300), Phin) + Phi_ = np.logspace(np.log10(1.0e-9), np.log10(500), Phin) base_mfp_=Phi_ - integral_overD = np.zeros((Phin)) + integral_overD_ = np.zeros((Phin, 2)) + print("Creating a lookup table for the steady state solute flow") for i, Phi in enumerate(Phi_): - print(i) - integral_overD[i] = PerirhizalPython.integral_overDiffusion_(Phi, sp) - np.savez(filename, integral_overD = integral_overD, base_mfp_ = base_mfp_, soil = list(sp)) - self.lookup_table_solutes = RegularGridInterpolator((base_mfp_, np.array([0,1])) , integral_overD) + print(i, " / ", Phin) + integral_overD_[i,0] = PerirhizalPython.integral_overDiffusion_(Phi, sp) + integral_overD_[i,1] = integral_overD_[i,0] + np.savez(filename, integral_overD_ = integral_overD_, base_mfp_ = base_mfp_, soil = list(sp)) + self.lookup_table_solutes = RegularGridInterpolator((base_mfp_,[0,1]) , integral_overD_) self.sp = sp - return integral_overD, base_mfp_ + return integral_overD_, base_mfp_ def create_integralconcentration_lookup(self, filename, Ds_, sp): """ @@ -631,24 +634,26 @@ def create_integralconcentration_lookup(self, filename, Ds_, sp): """ #repeat steady state case - integral_overD, base_mfp_ = self.create_integralDiffusion_lookup(filename, self.sp) + integral_overD_, base_mfp_ = self.create_integralDiffusion_lookup(filename, self.sp) n_Ds = len(Ds_) - Phin = 300 + Phin = 200 Phi_ = np.logspace(np.log10(1.0e-6), np.log10(300), Phin) outer_mfp_=Phi_ inner_mfp_=Phi_ R_sr_ = np.zeros((n_Ds,Phin,Phin)) + print("Creating a lookup table for the steady rate solute flow") for i, Ds in enumerate(Ds_): print("starting with diffusion coefficient number ", str(i+1), ":") + C_d = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) for j, Phi_in in enumerate(inner_mfp_): print(j) for k, Phi_out in enumerate(outer_mfp_): - R_sr_[i,j,k] = PerirhizalPython.integral_overconcentration_(Ds, Phi, sp) + R_sr_[i,j,k] = self.integral_overconcentration_(C_d, Phi_in, Phi_out, 100.0, sp) - np.savez(filename, integral_overD=integral_overD, base_mfp_=base_mfp_, D_s_ = D_s_, outer_mfp_ = outer_mfp_, inner_mfp_ = inner_mfp_, R_sr_ = R_sr_, soil = list(sp)) - self.lookup_table_sr_solutes = RegularGridInterpolator((base_mfp_, np.array([0,1])) , integral_overD) + np.savez(filename, integral_overD_=integral_overD_, base_mfp_=base_mfp_, Ds_ = Ds_, outer_mfp_ = outer_mfp_, inner_mfp_ = inner_mfp_, R_sr_ = R_sr_, soil = list(sp)) + self.lookup_table_sr_solutes = RegularGridInterpolator((Ds_, inner_mfp_, outer_mfp_) , R_sr_) self.sp = sp def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): diff --git a/src/functional/PlantHydraulicParameters.py b/src/functional/PlantHydraulicParameters.py index 55e4dd461..3497b636d 100644 --- a/src/functional/PlantHydraulicParameters.py +++ b/src/functional/PlantHydraulicParameters.py @@ -120,7 +120,7 @@ def read_parameters(self, filename): self.setKrValues(json_dict["krPerSegment"]) # can be empty self.setKxValues(json_dict["kxPerSegment"]) for ot in [int(pb.OrganTypes.root), int(pb.OrganTypes.stem), int(pb.OrganTypes.leaf)]: - for st in range(0, self.maxSubTypes): + for st in range(0, self.maxSubTypes-1): age_ = json_dict["kx_ages"][str(ot)][st] value_ = json_dict["kx_values"][str(ot)][st] self.setKxAgeDependent(age_, value_, st, ot) diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 0a7beafac..ec2e72437 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -82,11 +82,11 @@ sp = vg.Parameters(hydrus_loam) peri.set_soil(sp) -#create the big lookup table +#create the lookup table #t = time.time() #peri.create_lookup("results/hydrus_loam", sp) #elapsed_time = time.time() - t -#print("Creating the big lookup table took about ", elapsed_time) # +#print("Creating the lookup table took about ", elapsed_time) # peri.open_lookup("results/hydrus_loam") @@ -99,8 +99,15 @@ peri.open_global_lookup("results/"+peri.water_filename) +# create lookup tables for the solute flow +Ds_=np.logspace(np.log10(1.0e0), np.log10(1.0e1), 2) +#peri.create_integralDiffusion_lookup("results/hydrus_loam_ss_solutes", sp) +#peri.create_integralconcentration_lookup("results/hydrus_loam_sr_solutes", Ds_, sp) + +# open lookup tables for the solute flow +peri.open_lookup_solutes("results/hydrus_loam_ss_solutes") +peri.open_lookup_sr_solutes("results/hydrus_loam_sr_solutes") -#hsr = peri.soil_root_interface_potentials(rx, sx, inner_kr, rho) #numerically solve each root segment, this is the reference solution hsr = np.array([peri.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], peri.sp) for i in range(0, len(rx))]) @@ -112,7 +119,7 @@ tests.loc[:,"hsr_lookup"] = hsr1 print("Norm of the difference to basic lookup table:", LA.norm(hsr-hsr1)) -#the alternative implementation +#the alternative global implementation hsr3 = peri.soil_root_interface_potentials_table_global(rx, sx, inner_kr, rho) tests.loc[:,"hsr_global"] = hsr3 print("Norm of the difference to global lookup table:", LA.norm(hsr-hsr3)) @@ -122,6 +129,7 @@ Phi_root = np.array([vg.fast_mfp[sp](hsr[i]) for i in range(len(hsr))]) Phi_soil = np.array([vg.fast_mfp[sp](sx[i]) for i in range(len(sx))]) tests.loc[:,"waterflow"] = waterflow +Ds = 1.0e1 #base solutes tests.loc[:,"sol_c"] = c_sol @@ -138,30 +146,7 @@ tests.loc[:,"sol_U_sr"] = np.array([Vmax[i]*solutes_sr[i]/(Km[i]+solutes_sr[i])*1e6 for i in range(len(solutes_sr))]) -# generate the values for the solute (and water?) uptake with dumux rosi: - -#print(tests) - -# for i in range(ntests): - # cyl = RichardsWrapper(RichardsNCCylFoam()) - - # points = [0.03,0.03*tests["rho"].iloc[0]] # radius of discretisation - # NC = 100 # number of discretisations + 1 - # seg_length = 1 - # cyl.createGrid1d(points)# cm - # cyl.setParameter( "Soil.Grid.Cells",str( NC-1)) - # cyl.seg_length = seg_length - # cyl.setTopBC("noFlux") - # cyl.setBotBC("constantPressure") - # cyl.setTopBC_solute("noFlux") - # cyl.setBotBC_solute("michaelisMenten") - # cyl.setParameter("RootSystem.Uptake.Vmax",tests.loc[str(i),"Vmax"]) - # cyl.setParameter("RootSystem.Uptake.Km",tests.loc[str(i),"Km"]) - # Cells = cyl.getCellCenters_().reshape(-1) - # CellsStr = cyl.dumux_str(Cells/100)#cm -> m #changed by Erik - # cyl.setParameter("Soil.IC.Z",CellsStr)# m - # cyl.setParameter("Soil.IC.C"+str(j)+"Z",CellsStr) # m - # cyl.setCriticalPressure(-15000) # cm pressure head + print("All inputs:") print(tests[inputs]) @@ -171,77 +156,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# #print("solute tests") - -# #peri.create_integralDiffusion_lookup("hydrus_loam_solutes", peri.sp) -# peri.open_lookup_solutes("hydrus_loam_solutes") - -# labels_sol = ["Phi_root", "Phi_soil", "c_bulk", "Vmax", "Km", "Ds", "waterflow", "c_rootsoil"] - -# Intervals_sol = { - # "Phi_root": [0.0,50], - # "Phi_soil": [60,140], - # "c_bulk": [1.5e-8,1.5e-7], - # "Vmax": [3.5e-6,3.6e-6], - # "Km": [1.4e-7,1.5e-7], - # "waterflow": [0.0,0.0], - # } -# Intervals_sol = pd.DataFrame(Intervals_sol, index = ["min","max"]) - -# tests_sol = pd.DataFrame(index = range(ntests),columns = labels_sol) - -# for one_label in Intervals_sol.columns: - # min_val = Intervals_sol.loc["min",one_label] - # max_val = Intervals_sol.loc["max",one_label] - - # testvalues = (max_val - min_val) * np.random.rand(ntests) + min_val * np.ones(ntests) - # tests_sol[one_label] = testvalues - -# Phi_root = np.array(tests_sol.loc[:,"Phi_root"]) -# Phi_soil = np.array(tests_sol.loc[:,"Phi_soil"]) -# c_bulk = np.array(tests_sol.loc[:,"c_bulk"]) -# Vmax = np.array(tests_sol.loc[:,"Vmax"]) -# Km = np.array(tests_sol.loc[:,"Km"]) -# waterflow = np.array([(2*3.14*inner_kr[i]*(hsr[i]-rx[i])) for i in range(ntests)]) -# Ds = 1.9e-5 - -# #c_rootsoil = peri.soil_root_solutes(Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow) - -# tests_sol.loc[:,"c_rootsoil"] = c_rootsoil - - - - -# #print(tests_sol) diff --git a/tutorial/chapter7_coupled/example7_3_coupling_fp.py b/tutorial/chapter7_coupled/example7_3_coupling_fp.py index 259f20177..e22f8a0d5 100644 --- a/tutorial/chapter7_coupled/example7_3_coupling_fp.py +++ b/tutorial/chapter7_coupled/example7_3_coupling_fp.py @@ -78,7 +78,7 @@ def picker(x, y, z): # Perirhizal initialization peri = PerirhizalPython(hm.ms) # |\label{l73c:peri}| # peri.set_soil(vg.Parameters(loam)) # |\label{l73c:perisoil}| -peri.open_simp_lookup("results/hydrus_loam") # |\label{l73c:peritable}| +peri.open_lookup("results/hydrus_loam") # |\label{l73c:peritable}| outer_r = peri.get_outer_radii("length") # |\label{l73c:outer}| inner_r = peri.ms.radii diff --git a/tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py b/tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py index 7b14e3ce7..8ea092603 100644 --- a/tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py +++ b/tutorial/chapter7_coupled/example7_3_coupling_fp_mpi.py @@ -87,7 +87,7 @@ def picker(x, y, z): peri = PerirhizalPython(hm.ms) # |\label{l73c:peri}| # peri.set_soil(vg.Parameters(loam)) # |\label{l73c:perisoil}| home = Path.home() - peri.open_simp_lookup("results/hydrus_loam") # |\label{l73c:peritable}| + peri.open_lookup("results/hydrus_loam") # |\label{l73c:peritable}| outer_r = peri.get_outer_radii("length") # |\label{l73c:outer}| inner_r = peri.ms.radii diff --git a/tutorial/chapter7_coupled/example7_3_lookup.py b/tutorial/chapter7_coupled/example7_3_lookup.py index 932d924ad..dbe3c4e6c 100644 --- a/tutorial/chapter7_coupled/example7_3_lookup.py +++ b/tutorial/chapter7_coupled/example7_3_lookup.py @@ -9,4 +9,4 @@ filename = "hydrus_loam" sp = vg.Parameters(hydrus_loam) # |\label{l73l:soil_end}| vg.create_mfp_lookup(sp) # |\label{l73l:mfp}| -peri.create_lookup_simp("results/" + filename, sp) # |\label{l73l:lookup}| +peri.create_lookup("results/" + filename, sp) # |\label{l73l:lookup}| From 6fc69cd2a9a86b2593f5fb0ee19978374197b513 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sat, 2 May 2026 21:30:11 +0200 Subject: [PATCH 09/41] store changes before merge --- src/functional/Perirhizal.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index c1c6d58da..411f5d7d2 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -86,11 +86,11 @@ def open_lookup_solutes(self, filename): def open_lookup_sr_solutes(self, filename): """opens an additional look-up table from a file to quickly find soil root solute concentrations in the steady rate case""" - npzfile = np.load(filename + ".npz") + npzfile_sr = np.load(filename + ".npz") self.open_lookup_solutes(filename) #repeat steady state case - R_sr_ = npzfile["R_sr_"] - Ds_, outer_mfp_, inner_mfp_ = npzfile["Ds_"], npzfile["outer_mfp_"], npzfile["inner_mfp_"] - soil = npzfile["soil"] + R_sr_ = npzfile_sr["R_sr_"] + Ds_, outer_mfp_, inner_mfp_ = npzfile_sr["Ds_"], npzfile_sr["outer_mfp_"], npzfile_sr["inner_mfp_"] + soil = npzfile_sr["soil"] self.lookup_table_sr_solutes = RegularGridInterpolator((Ds_, inner_mfp_, outer_mfp_) , R_sr_) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) vg.create_mfp_lookup(self.sp) # does this have to be repeated here? @@ -269,20 +269,24 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w #repeat from steady state if self.lookup_table_solutes: F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, len(c_bulk))] + #F = np.zeros(len(c_bulk)) + #for i in range(0, len(c_bulk)): + # print(Phi_soil[i],Phi_root[i]) + # F[i] = (self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) else: F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] #compute a prefactor C_d = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) - if approximate_rho: if self.lookup_table_sr_solutes: - R_sr=[self.lookup_table_sr_solutes((C_d, Phi_root[i], Phi_soil[i])) for i in range(0, len(c_bulk))] + #print(C_d, Phi_root, Phi_soil) + R_sr=[self.lookup_table_sr_solutes((Ds, max(Phi_root[i],1.0e-5), min(Phi_soil[i],299))) for i in range(0, len(c_bulk))] else: - R_sr=[self.integral_overconcentration_(C_d, Phi_root[i], Phi_soil[i], 100.0, self.sp) for i in range(0, len(c_bulk))] + R_sr=[self.integral_overconcentration_(Ds, Phi_root[i], Phi_soil[i], 100.0, self.sp) for i in range(0, len(c_bulk))] else: - R_sr=[self.integral_overconcentration_(C_d, Phi_root[i], Phi_soil[i], rho[i], self.sp) for i in range(0, len(c_bulk))] + R_sr=[self.integral_overconcentration_(Ds, Phi_root[i], Phi_soil[i], rho[i], self.sp) for i in range(0, len(c_bulk))] watercontent = vg.water_content(vg.fast_imfp[sp](Phi_soil), self.sp) #mean watercontent of the perirhizal zone #reuse steady state case @@ -293,6 +297,9 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w a2=(F_tilde[i]*R_sr[i]-watercontent[i])/(F_tilde[i]*R_sr[i]*waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 + if q>0: + print("q>0",Km[i],a1,F_tilde[i],R_sr[i],c_bulk[i],watercontent[i]) + q=0 rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) @@ -692,7 +699,7 @@ def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): inner_kr_b = np.divide(inner_kr_,b) base_mfp = - (inner_kr_b * rx + vg.fast_mfp[self.sp](sx)) - rsx = self.lookup_table((inner_kr_b, base_mfp)) + rsx = self.lookup_table((inner_kr_b, np.array([max(min(base_mfp[i],500),-500) for i in range(len(base_mfp))]))) rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential except: if np.max(rx) > 0: From 7a6798836c7bde4e221fd12e120bea26027424ed Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Tue, 12 May 2026 13:44:39 +0200 Subject: [PATCH 10/41] debugging 1p2C doesnt work --- src/functional/Perirhizal.py | 4 + test/test_perirhizal.py | 757 ++++++++++++++++++++++++++++------- 2 files changed, 626 insertions(+), 135 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 9ab013d8c..bc098e91b 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -411,6 +411,10 @@ def create_lookup_mpi(self, filename, sp): sp van genuchten soil parameters, , call vg.create_mfp_lookup(sp) before """ + + if rank == 0: + print("The function *create_lookup_mpi* is deprecated, use *create_lookup* without mpi instead.") + from mpi4py import MPI comm = MPI.COMM_WORLD diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index ec2e72437..6590a8ed9 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -1,157 +1,644 @@ -# this is a small test file by Erik Kopp to test the alternative implementation of the perirhizal resistances and the perirhizal diffusion +# this is a test file by Erik Kopp to test the alternative implementation of the perirhizal resistances and the perirhizal diffusion import sys; sys.path.append("../src/functional") +sys.path.append("../../dumux-rosi/build-cmake/cpp/python_binding/") +sys.path.append("../../../dumuxtest/dumux/dumux-rosi/build-cmake/cpp/python_binding/") #from plantbox import Perirhizal from plantbox.functional.Perirhizal import PerirhizalPython from numpy import linalg as LA import plantbox.functional.van_genuchten as vg from rosi.richards_flat import RichardsFlatWrapper as RichardsWrapper # Python part, macroscopic soil model #from richards import RichardsWrapper # Python part -#from richards_no_mpi import RichardsNoMPIWrapper # Python part of cylindrcial -from rosi.rosi_richardsnc_cyl import RichardsNCCylFoam # C++ part (Dumux binding), macroscopic soil model +from rosi.richards_no_mpi import RichardsNoMPIWrapper # Python part of cylindrcial +from rosi.rosi_richardsnc_cyl import RichardsNCCylFoam as RichardsNC_cyl # C++ part (Dumux binding), macroscopic soil model +#from rosi_richardsnc_cyl import RichardsNCCylFoam as RichardsNC_cyl # C++ part (Dumux binding) +from rosi_richards22c import RichardsNCSPILU as RichardsNCSP #test + import Perirhizal import pandas as pd import numpy as np import time -# generate random inputs for one perirhizal segment - -""" - rx xylem matric potential [cm] - sx bulk soil matric potential [cm] - inner_kr root radius times hydraulic conductivity [cm/day] - rho geometry factor [1] (outer_radius / inner_radius) - c_sol concentration of solutes in the bulk soil - Vmax Michaelis Menten Kinetics maximal solute uptake rate - Km Michaelis Menten Kinetics half saturation - sp soil parameter: van Genuchten parameter set (type vg.Parameters) - sp_theta_r - sp_theta_s - sp_n - sp_alpha - sp_Ksat -""" -labels = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km","hsr","hsr_lookup","hsr_global","waterflow","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] - -inputs = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km"] -outputs = ["hsr","hsr_lookup","hsr_global","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] -outputs = ["rx","sx","waterflow","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] - -#kr is assumed to lie between .5 and 5e-6 cm/hPa/d = 0.5 to 5e-6 1/d -#r is assumed to be 0.03 -#multiply this number by 10 in order to not fall under the threshhold at which the soil hydraulic potential is chosen as a default - -Intervals = { - "rx": [-14000,-10], - "sx": [-1000,-10], - "inner_kr": [1.5e-7,1.5e-6], - "rho": [10,199.0], - "c_sol": [1.0e-9,1.0e-5], - "Vmax": [1.0e-11,1.0e-10], - "Km": [1.0e-8,1.0e-6], - } -Intervals = pd.DataFrame(Intervals, index = ["min","max"]) - -ntests = 10 -tests = pd.DataFrame(index = range(ntests),columns = labels) - -for one_label in Intervals.columns: - min_val = Intervals.loc["min",one_label] - max_val = Intervals.loc["max",one_label] - - testvalues = (max_val - min_val) * np.random.rand(ntests) + min_val * np.ones(ntests) - tests[one_label] = testvalues - -peri = PerirhizalPython() -#peri = PerirhizalPython(Perirhizal) - - -rx = np.array(tests.loc[:,"rx"]) #* (-1) -sx = np.array(tests.loc[:,"sx"]) #* (-1) -inner_kr = np.array(tests.loc[:,"inner_kr"]) -rho = np.array(tests.loc[:,"rho"]) -c_sol = np.array(tests.loc[:,"c_sol"]) -Vmax = np.array(tests.loc[:,"Vmax"]) -Km = np.array(tests.loc[:,"Km"]) -Ds = 1.0e1 -r_root = 0.03 - -hydrus_loam = [0.078, 0.43, 0.036, 1.56, 24.96] -sp = vg.Parameters(hydrus_loam) -peri.set_soil(sp) - -#create the lookup table -#t = time.time() -#peri.create_lookup("results/hydrus_loam", sp) -#elapsed_time = time.time() - t -#print("Creating the lookup table took about ", elapsed_time) # - -peri.open_lookup("results/hydrus_loam") - - -#create the global lookup table -#t = time.time() -#peri.create_lookup_global("results/"+peri.water_filename, sp) -#elapsed_time = time.time() - t -#print("Creating the global lookup table took about ", elapsed_time) # - -peri.open_global_lookup("results/"+peri.water_filename) - -# create lookup tables for the solute flow -Ds_=np.logspace(np.log10(1.0e0), np.log10(1.0e1), 2) -#peri.create_integralDiffusion_lookup("results/hydrus_loam_ss_solutes", sp) -#peri.create_integralconcentration_lookup("results/hydrus_loam_sr_solutes", Ds_, sp) - -# open lookup tables for the solute flow -peri.open_lookup_solutes("results/hydrus_loam_ss_solutes") -peri.open_lookup_sr_solutes("results/hydrus_loam_sr_solutes") - - -#numerically solve each root segment, this is the reference solution -hsr = np.array([peri.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], peri.sp) for i in range(0, len(rx))]) -tests.loc[:,"hsr"] = hsr -print("norm of the root soil matrix potentials: ", LA.norm(hsr)) - -#the standard implementation -hsr1= peri.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) -tests.loc[:,"hsr_lookup"] = hsr1 -print("Norm of the difference to basic lookup table:", LA.norm(hsr-hsr1)) - -#the alternative global implementation -hsr3 = peri.soil_root_interface_potentials_table_global(rx, sx, inner_kr, rho) -tests.loc[:,"hsr_global"] = hsr3 -print("Norm of the difference to global lookup table:", LA.norm(hsr-hsr3)) - -#waterflow = 2*3.14*np.multiply((hsr-rx),inner_kr)/r_root -waterflow = 2*3.14*np.multiply((sx-rx),inner_kr)#/r_root -Phi_root = np.array([vg.fast_mfp[sp](hsr[i]) for i in range(len(hsr))]) -Phi_soil = np.array([vg.fast_mfp[sp](sx[i]) for i in range(len(sx))]) -tests.loc[:,"waterflow"] = waterflow -Ds = 1.0e1 - -#base solutes -tests.loc[:,"sol_c"] = c_sol -tests.loc[:,"sol_U"] = np.array([Vmax[i]*c_sol[i]/(Km[i]+c_sol[i])*1e6 for i in range(len(c_sol))]) - -# steady state solutes -solutes_ss = peri.soil_root_solutes_ss_(Phi_root, Phi_soil, c_sol, Vmax, Km, Ds, waterflow, peri.sp) -tests.loc[:,"sol_c_ss"] = solutes_ss -tests.loc[:,"sol_U_ss"] = np.array([Vmax[i]*solutes_ss[i]/(Km[i]+solutes_ss[i])*1e6 for i in range(len(solutes_ss))]) - -# steady rate solutes -solutes_sr = peri.soil_root_solutes_sr_(Phi_root, Phi_soil, rho, c_sol, Vmax, Km, Ds, waterflow, peri.sp) -tests.loc[:,"sol_c_sr"] = solutes_sr -tests.loc[:,"sol_U_sr"] = np.array([Vmax[i]*solutes_sr[i]/(Km[i]+solutes_sr[i])*1e6 for i in range(len(solutes_sr))]) +# run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model + +n_tests = 10 #try everything here for this many random parameter sets +do_computation = True #should the computation be run or take the data from a saved file + +# general parameters + +max_time = 10 #d +n_times = 100 # number of time intervals +times = np.linspace(0,max_time,n_times)[1:] +r_prhiz = 1 # cm +r_root = 0.02 # cm +rho = r_prhiz / r_root +NC = 101 # number of spatial discretisations +n_sp = NC - 1 + +dt = max_time / n_times + +#space for the oupputs +watercontent_dumux = np.zeros((n_tests, n_times, n_sp+1)) +soluteconcentration_dumux = np.zeros((n_tests, n_times, n_sp+1)) +soluteconcentration_steadystate = np.zeros((n_tests, n_times, n_sp+1)) +watercontent_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) +soluteconcentration_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) + +soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil + +def load_constants(cyl): + + cyl.molarMassC = 12.011 + cyl.molarMassN = 14.0 + cyl.yr_per_d = 1/365 # [yr/d] + cyl.m3_per_cm3 = 1e-6; # m3/cm3 + cyl.cm3_per_m3 = 1e6; # cm3/m3 + cyl.cm3_per_L = 1e3; # cm3/dm3 + + cyl.kgC_per_mol = (1/1000) * cyl.molarMassC + cyl.kgN_per_mol = (1/1000) * cyl.molarMassN + + yr_per_d = 1/365 # [yr/d] + m3_per_cm3 = 1e-6; # m3/cm3 + cm3_per_m3 = 1e6; # cm3/m3 + + molMarssW = 18 # g/mol + rhoW = 1 #g/cm3 + rhoWM = rhoW/molMarssW #[g/cm3]*[g/mol] = mol/cm3 + cyl.rhoWM = rhoWM + mlFr=1/rhoWM + + cyl.molarMassC = 12.011 + cyl.molarMassN = 14.0 + cyl.mg_per_molC = cyl.molarMassC * 1000. + cyl.mg_per_molN = cyl.molarMassN * 1000. + yr_per_d = 1/365 # [yr/d] + m3_per_cm3 = 1e-6; # m3/cm3 + cm3_per_m3 = 1e6; # cm3/m3 + + molMarssW = 18 # g/mol + molMassMulC = 30 #g/mol + rhoW = 1 #g/cm3 + rhoWM = rhoW/molMarssW #[g/cm3]*[g/mol] = mol/cm3 + cyl.mlFr=1/rhoWM #only water phase is relevant right now + + + + + return cyl + +def getSoilTextureAndShape(s, soilVG): + """ soil shape and texture data + to adapt according to the soil represented + """ + + constants = {'solidDensity': 2650, # kg/m3 + 'solidMolarMass': 60.08e-3 #kg/mol + } + paramSet = pd.DataFrame() + + solidDensity = 2650 # [kg/m^3 solid] #taken from google docs TraiRhizo + solidMolarMass = 60.08e-3 # [kg/mol] + # theta_r, theta_s, alpha, n, Ks + #soilVG = [paramSet['soilVG_theta_r'],paramSet['soilVG_theta_s'],paramSet['soilVG_alpha'],paramSet['soilVG_n'],paramSet['soilVG_Ks']] + soilTextureAndShape = { + "solidDensity":solidDensity, + 'solidMolarMass': solidMolarMass, + 'soilVG':soilVG} + #common transformations + + soilTexture =soilTextureAndShape + + s.solidDensity = soilTexture['solidDensity'] #[kg/m^3 solid] + s.solidMolarMass = soilTexture['solidMolarMass']# [kg/mol] + s.soil = soilTexture['soilVG'] + + s.vg_soil = vg.Parameters(s.soil) + # [mol / m3 solid] =[kg/m^3 solid] / [kg/mol] + s.solidMolDensity = s.solidDensity/s.solidMolarMass + # [mol / m3 scv] = [mol / m3 solid] * [m3 solid /m3 space] + s.bulkDensity_m3 = s.solidMolDensity*(1.- s.vg_soil.theta_S) + # (kg/m3) * (volume soil / total volume) * (g/kg) * (m3/cm3) + s.bulkMassDensity_gpercm3 = s.solidDensity*(1.- s.vg_soil.theta_S)*1000*1e-6 + + s.masspercm3 = s.solidDensity * 1.e-6 + return soilTexture + +def run_perirhizal_test(): + + #space for the outputs + water_dumux = np.zeros((n_times, n_sp+1)) + solute_dumux = np.zeros((n_times, n_sp+1)) + water_ss = np.zeros((n_times, n_sp+1)) + solute_ss = np.zeros((n_times, n_sp+1)) + water_sr = np.zeros((n_times, n_sp+1)) + solute_sr = np.zeros((n_times, n_sp+1)) + + mean_water = np.zeros((n_times)) + mean_solutes = np.zeros((n_times)) + + # determine some random parameters + initial_waterpotential = -100 + np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? + initial_soluteconcentration = 1e-6*(1.0+np.random.rand()) #mol/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? + print("initial_soluteconcentration",initial_soluteconcentration) + + # root conductivity and solute uptake parameters, it is chosen to be constant throughout the entire simulation time + root_conductivity = 1e-4 #1/d + inner_kr = root_conductivity * r_root + Vmax = 1.0e-10 #mol/d + Km = 1.0e-9 #mol/cm3 + + DS_W = 1.902e-5 #cm2/s + Ds = DS_W * 3600 * 24 #cm2/d + + # the xylem matrix potential varies over time (keep it low so that there is little to no outflow of water) + rx_t = lambda t : -500+200*np.sin(t) #cm + + # load the perirhizal model + peri = PerirhizalPython() + sp = vg.Parameters(soilVG) + peri.set_soil(sp) + #no lookup tables are used here as there arent many simulations + + lb = 0.5 + points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), + NC, base = lb) + CC = np.array([(points[i] + points[i+1])/2 for i in range(len(points)-1)]) + + + + # initialise the dumux model + print("test0") + #cyl = RichardsNoMPIWrapper(RichardsNCCylFoam()) + cyl = RichardsWrapper(RichardsNC_cyl()) + cyl = load_constants(cyl) + soilTexture = getSoilTextureAndShape(cyl, soilVG) + print("test005") + # cyl = RichardsNoMPIWrapper(RichardsNC_cyl()) + c2 = [cyl.mlFr * CC[i] for i in range(NC-1)] + #cyl = RichardsWrapper(RichardsCylFoam()) + #cyl.initialize() + cyl.createGrid1d(points) + initial=-10 + cyl.setHomogeneousIC(initial) # cm pressure head + #cyl.setICZ_solute(cyl.dumux_str([q for q in c2])) + cyl.setICZ_solute(str([q for q in c2])) + print("test006") + cyl.initialize() + + cyl.setVGParameters([soilVG]) + cyl.setOuterBC("fluxCyl", 0.) # [cm/day] + cyl.setInnerBC("fluxCyl", 0.) # [cm/day] + print("test007") + #cyl.initializeProblem() + cyl.maxDt = 3500/(3600*24) # soilModel.maxDt_1DS + #cyl.setParameter("Soil.mucilCAffectsW", "true") + cyl.setParameter("Problem.verbose", "0") + cyl.setParameter("Newton.Verbosity", "0") + cyl.initializeProblem(maxDt=cyl.maxDt ) + print("test008") + critP=-15000 + cyl.setCriticalPressure(critP) # cm pressure head + #cyls.append(cyl) + print("test0.1") + seg_length = 1 #cm + + if True: + cyl.setParameter("Newton.EnableResidualCriterion", "true") # sometimes helps, sometimes makes things worse + cyl.setParameter("Newton.EnableAbsoluteResidualCriterion", "true") + cyl.setParameter("Newton.SatisfyResidualAndShiftCriterion", "true") + cyl.setParameter("Newton.MaxRelativeShift", "1e-9")# reset value + #cyl.initialize(verbose = False) # No parameter file found. Continuing without parameter file. + #cyl.initialize() + print("test1") + #cyl.initializeProblem(maxDt=cyl.maxDt ) + cyl = load_constants(cyl) + soilTexture = getSoilTextureAndShape(cyl, soilVG) + + c2 = [cyl.mlFr * CC[i] for i in range(NC-1)] + + cyl.createGrid1d(points)# cm + cyl.doSoluteFlow = True + cyl.noAds = False + + #set soil parameters + cyl.solidDensity = soilTexture['solidDensity'] #[kg/m^3 solid] + cyl.solidMolarMass = soilTexture['solidMolarMass']# [kg/mol] + cyl.soil = soilTexture['soilVG'] + + cyl.setParameter( "Soil.MolarMass", str(cyl.solidMolarMass)) + cyl.setParameter( "Soil.solidDensity", str(cyl.solidDensity)) + #cyl.setVGParameters([cyl.soil]) + + minD=1.0e-12#minimal diffusion coefficient, I think this will be numerically good + #cyl.setParameter("1.Component.LiquidDiffusionCoefficient", str(DS_W/10000+minD)) #cm^2/s -> m^2/s + #todo: reactivate the diffusion coefficient + + #set michaelis menten uptake parameters + cyl.setParameter("RootSystem.Uptake.Vmax", str(Vmax)) + cyl.setParameter("RootSystem.Uptake.Km", str(Km)) + + #cyl = setSoilParam(cyl,paramSet) # here the soil parameters are set out of paramSet + #cyl = setBiochemParam(cyl,paramSet) # here the biochemical parameters are set out of paramSet + print("test2") + #cyl.initializeProblem(maxDt=cyl.maxDt ) + doCells = False + if doCells: + Cells = (cyl.getCellCenters_().reshape(-1)) + else: + Cells = [] + cyl.setParameter("Flux.UpwindWeight", "1") + cyl.setParameter("SpatialParams.Temperature","293.15") # todo: redefine at each time step + cyl.setParameter("Soil.BC.dzScaling", "1") + #cyl.setParameter( "Soil.css1Function", str(9)) # todo: can I activate this? + cyl.setParameter("Problem.verbose", "0") + cyl.setParameter("Problem.reactionExclusive", "0") + cyl.setParameter("Soil.CriticalPressure", str(-15000)) + cyl.seg_length = seg_length + cyl.setParameter("Problem.segLength", str(seg_length)) # cm + cyl.l = seg_length + cyl.setParameter( "Soil.Grid.Cells",str( NC-1)) # -1 to go from vertices to cell (dof) # I am unsure what this comment means + cyl.setParameter("Newton.MaxTimeStepDivisions", + str( 100) ) + cyl.setParameter("Newton.MaxSteps", + str( 100) ) + cyl.setParameter("Newton.EnableChop", "true") + print("test3") + #cyl.initializeProblem(maxDt=cyl.maxDt ) + #boundary conditions + #water + cyl.setParameter( "Soil.BC.Bot.Type", str(int(3))) + cyl.setParameter( "Soil.BC.Top.Type", str(int(3))) + cyl.setParameter( "Soil.BC.Bot.Value", str(0.0)) #will be prescribed at each timestep + cyl.setParameter( "Soil.BC.Top.Value", str(0.0)) + print("test3.1") + #solutes + cyl.setParameter( "Soil.BC.Bot.SType", str(int(3))) # TODO change this to 8 as Michaelis mnetne + cyl.setParameter( "Soil.BC.Top.SType", str(int(3))) + cyl.setParameter( "Soil.BC.Bot.CValue", str(0.0)) #Michaelis Menten parameters are set elsewhere + cyl.setParameter( "Soil.BC.Top.CValue", str(0.0)) + + print("test3.2") + #cyl.initializeProblem(maxDt=cyl.maxDt ) + + #initial conditions + cyl.setHomogeneousIC(initial_waterpotential) + #cyl.setParameter("Soil.IC.P", cyl.dumux_str(initial_waterpotential))# cm #this is where the initial matrix potential and with it the water content are set + #molar_fraction = initial_soluteconcentration / 62 * 18 /1 # / molar mass NO3 * molarMass Water + #print("molar_fraction",molar_fraction) + #cyl.setICZ_solute(c=[molar_fraction]) + theta = 0.3 #change this after testing + c2 = [cyl.mlFr * initial_soluteconcentration/cyl.mg_per_molC*cyl.masspercm3/theta for i in range(NC-1)] + #mlFr*paramSet['Soil.IC.C1']/cyl.mg_per_molC*masspercm3/theta + temp = np.array(c2)*initial_soluteconcentration + #temp2 = cyl.dumux_str(temp.tolist()) + temp2 = cyl.dumux_str(c2) + #cyl.initializeProblem(maxDt=cyl.maxDt ) + print("IC",temp2) + #cyl.initialize() + #cyl.initializeProblem(maxDt=cyl.maxDt ) + # + #cyl.initialize() + #cyl.setParameter("Soil.IC.C1",cyl.dumux_str([q for q in c2])) + #cyl.setParameter("Soil.IC.C", temp2)# cm #does this work without spatial resolution + #cyl.setParameter("Soil.IC.C", initial_soluteconcentration)# cm #does this work without spatial resolution + + print("test3.3") + cyl.initializeProblem(maxDt=cyl.maxDt ) + + if len(Cells) > 0:#in case we update a cylinder, to allow dumux to do interpolation + assert(len(c2)==len(Cells)) + CellsStr = cyl.dumux_str(Cells/100)#cm -> m #changed by Erik + cyl.setParameter("Soil.IC.Z",CellsStr)# m + if len(Cells)!= len(x): + print("Cells, x",Cells, x, len(Cells), len(x)) + raise Exception + + #j = 2 + for j in range( 1, cyl.numComp): + cyl.setParameter("Soil.IC.C"+str(j)+"Z",CellsStr) # m + if len(Cells)!= len( c2): + print("Cells, cAll[j-1]",Cells, c2, + len(Cells), len(c2), j) + raise Exception + print("test3.4") + cyl.maxDt = 3500/(3600*24) # soilModel.maxDt_1DS + #cyl.setParameter("Soil.mucilCAffectsW", "true") + cyl.setParameter("Problem.verbose", "0") + cyl.setParameter("Newton.Verbosity", "0") + cyl.initializeProblem(maxDt=cyl.maxDt ) + + #cyl.setParameter("Problem.doNeffect","true") + + cyl.eps_regularization = 1e-14 + #cyl.setRegularisation(cyl.eps_regularization, cyl.eps_regularization) + #which one is the relative error? + #pcEps capillary pressure regularisation, krEps relative permeabiltiy regularisation + #default 1e-6, 1e-6 + cyl.setRegularisation(cyl.eps_regularization, cyl.eps_regularization) + + cyl.setCriticalPressure(-15000) # cm pressure head + #cyl = setInitialConditions(cyl,paramSet) + + # run the dumux model + #get water and solute next to the root + print("test3.5") + current_rs_potential = cyl.getSolutionHead() #todo rmeove + print(current_rs_potential[0]) + print("test3.6") + #current_rs_potential = current_rs_potential[0] + current_rs_potential = initial_waterpotential + print("test3.7") + current_rs_concentration = cyl.getSolution(1) * cyl.rhoWM # todo remove + print(current_rs_concentration) + print(current_rs_concentration[0]) + print("test3.8") + #current_rs_concentration = current_rs_concentration[0] + current_rs_concentration = initial_soluteconcentration + print("test4") + for i in range(n_times): + rx = rx_t(i*dt) + #model root water uptake + root_wateruptake = root_conductivity * (rx - current_rs_potential) + water_dumux[i,0] = root_wateruptake + cyl.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) + + #model root solute uptake + root_soluteuptake = Vmax * current_rs_concentration * (Km + current_rs_concentration) + solute_dumux[i,0] = root_wateruptake + cyl.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) #todo set BC solute + print("test5") + cyl.solve(dt) + + # save outputs of dumux + print("test6") + water_dumux[i,1:] = cyl.getWaterContent() + solute_dumux[i,1:] = cyl.getSolution(0) + + mean_water = np.mean(water_dumux[i,1:]) + mean_solutes = np.mean(solutes_dumux[i,1:]) + + + current_rs_potential = cyl.getSolutionHead() + current_rs_potential = current_rs_potential[0] + current_rs_concentration = solute_dumux[i,1] + + + + + # run the alpah omega model on waterflow (steady rate) + # mean water content of the rhizosphere + total_water = 0 + total_solute = 0 + for j in range(NC-1): + total_water = total_water + water_dumux[i,j+1]*(points[j+1]**2 - points[j]**2) + total_solute = total_solute + water_dumux[i,j+1]*solute_dumux[i,j+1]*(points[j+1]**2 - points[j]**2) + mean_water = total_water / (points[NC-1]**2 - points[0]**2) + total_solute = total_solute / (points[NC-1]**2 - points[0]**2) + + #translate the mean water content to a mean matrix potential + sx = vg.pressure_head(mean_water, peri.sp) + + #start the steady rate solver + h_sr = peri.soil_root_interface_(rx, sx, inner_kr, rho, peri.sp) + + #compute both matrix flux potentials: + Phi_root = vg.fast_mfp[sp](h_sr) + Phi_soil = vg.fast_mfp[sp](s_x) + + #compute the spatial watercontents + Phi_A, Phi_C = peri.determine_mfp_function(Phi_root, Phi_soil, rho) + #outer Phi + Phi_out = Phi_A+Phi_C + + for j in range(NC-1): + s = CC[j] / r_prhiz + Phi = Phi_A*(s**2-np.log(s**2))+Phi_C + water_sr[i,j] = vg.water_content(vg.fast_imfp[sp](),peri.sp) + + + # run the alpha omega model on solute flow (both steady state and steady rate) + waterflow = root_wateruptake + result_solutes_ss = peri.soil_root_solutes_ss_(Phi_out, Phi_soil, total_solute, Vmax, Km, Ds, waterflow, peri.sp) + result_solutes_sr = peri.soil_root_solutes_sr_(Phi_root, Phi_soil, rho, total_solute, Vmax, Km, Ds, waterflow, peri.sp) + + solute_ss[i,1] = result_solutes_ss + solute_ss[i,0] = Vmax * result_solutes_ss / (Km + result_solutes_ss) + solute_sr[i,1] = result_solutes_sr + solute_sr[i,0] = Vmax * result_solutes_sr / (Km + result_solutes_sr) + + F0 = self.lookup_table_solutes((Phi_root[i],0)) # for the ratio of concentration next to the root to somewhere in the perirhizal zone + D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) + for j in range(NC-2): + s = CC[j+1] / r_prhiz + Phi_current = Phi_A*(s**2-np.log(s**2))+Phi_C + F = self.lookup_table_solutes((Phi_soil[i],0))-F0 + F_tilde=math.exp(D_tilde*F) + solute_ss[i,1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[i,0] / waterflow + solute_sr[i,1] = result_solutes_sr * F_tilde + (1-F_tilde) * solute_sr[i,0] / waterflow + + + return water_dumux, solute_dumux, solute_ss, water_sr, solute_sr + +if do_computation: + # save everything in the np arrays + for i in range(n_tests): + water_dumux, solute_dumux, solute_ss, water_sr, solute_sr = run_perirhizal_test() + watercontent_dumux[i,:,:]=water_dumux[:,:] + soluteconcentration_dumux[i,:,:]=solute_dumux[:,:] + soluteconcentration_steadystate[i,:,:]=solute_ss[:,:] + watercontent_steadyrate[i,:,:]=water_sr[:,:] + soluteconcentration_steadyrate[i,:,:]=solute_sr[:,:] + + np.savez("test_perirhizal.npz", watercontent_dumux=watercontent_dumux, soluteconcentration_dumux=soluteconcentration_dumux, soluteconcentration_steadystate=soluteconcentration_steadystate, watercontent_steadyrate=watercontent_steadyrate, soluteconcentration_steadyrate=soluteconcentration_steadyrate) + +else: + simulation_results = np.load("test_perirhizal.npz") + watercontent_dumux = simulation_results["watercontent_dumux"] + soluteconcentration_dumux = simulation_results["soluteconcentration_dumux"] + soluteconcentration_steadystate = simulation_results["soluteconcentration_steadystate"] + watercontent_steadyrate = simulation_results["watercontent_steadyrate"] + soluteconcentration_steadyrate = simulation_results["soluteconcentration_steadyrate"] + + +# compare both for the differint means of water / solute content +run = 5 +timestep = 10 + +#plot water and nitrogen in that one voxel +fig, ax1 = figure_style.subplots12(1, 1) +ax2 = ax1.twinx() + +water_dumux = watercontent_dumux[run, timestep, :] +water_perirhizal = watercontent_steadyrate[run, timestep, :] +solute_dumux = soluteconcentration_dumux[run, timestep, :] +solute_steadystate = soluteconcentration_steadystate[run, timestep, :] +solute_steadyrate = soluteconcentration_steadyrate[run, timestep, :] +linestyle_dumux = "solid" +linestyle_steadystate = "dotted" +linestyle_steadyrate = "dashed" + +ax1.plot(times, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") +ax1.plot(times, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") +ax2.plot(times, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") +ax2.plot(times, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") +ax2.plot(times, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + +ax1.set_xlabel("time (day)") +ax1.set_ylabel("water") +ax2.set_ylabel("nitrogen concentration") +ax1.legend(["watercontent cm3/cm3"], loc="upper left") +ax2.legend(["nitrogen concentration mol/cm3"], loc="upper right") + +ax1.legend(loc="upper left") +ax2.legend(loc="upper right") +#np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +plt.show() + + + + +# #---old stuff below + + +# # generate random inputs for one perirhizal segment + +# """ + # rx xylem matric potential [cm] + # sx bulk soil matric potential [cm] + # inner_kr root radius times hydraulic conductivity [cm/day] + # rho geometry factor [1] (outer_radius / inner_radius) + # c_sol concentration of solutes in the bulk soil + # Vmax Michaelis Menten Kinetics maximal solute uptake rate + # Km Michaelis Menten Kinetics half saturation + # sp soil parameter: van Genuchten parameter set (type vg.Parameters) + # sp_theta_r + # sp_theta_s + # sp_n + # sp_alpha + # sp_Ksat +# """ +# labels = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km","hsr","hsr_lookup","hsr_global","waterflow","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] + +# inputs = ["rx","sx","inner_kr","rho","c_sol","Vmax","Km"] +# outputs = ["hsr","hsr_lookup","hsr_global","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] +# outputs = ["rx","sx","waterflow","sol_c","sol_c_ss","sol_c_sr","sol_U","sol_U_ss","sol_U_sr"] + +# #kr is assumed to lie between .5 and 5e-6 cm/hPa/d = 0.5 to 5e-6 1/d +# #r is assumed to be 0.03 +# #multiply this number by 10 in order to not fall under the threshhold at which the soil hydraulic potential is chosen as a default + +# Intervals = { + # "rx": [-14000,-10], + # "sx": [-1000,-10], + # "inner_kr": [1.5e-7,1.5e-6], + # "rho": [10,199.0], + # "c_sol": [1.0e-9,1.0e-5], + # "Vmax": [1.0e-11,1.0e-10], + # "Km": [1.0e-8,1.0e-6], + # } +# Intervals = pd.DataFrame(Intervals, index = ["min","max"]) + +# ntests = 10 +# tests = pd.DataFrame(index = range(ntests),columns = labels) + +# for one_label in Intervals.columns: + # min_val = Intervals.loc["min",one_label] + # max_val = Intervals.loc["max",one_label] + + # testvalues = (max_val - min_val) * np.random.rand(ntests) + min_val * np.ones(ntests) + # tests[one_label] = testvalues + +# peri = PerirhizalPython() +# #peri = PerirhizalPython(Perirhizal) + + +# rx = np.array(tests.loc[:,"rx"]) #* (-1) +# sx = np.array(tests.loc[:,"sx"]) #* (-1) +# inner_kr = np.array(tests.loc[:,"inner_kr"]) +# rho = np.array(tests.loc[:,"rho"]) +# c_sol = np.array(tests.loc[:,"c_sol"]) +# Vmax = np.array(tests.loc[:,"Vmax"]) +# Km = np.array(tests.loc[:,"Km"]) +# Ds = 1.0e1 +# r_root = 0.03 + +# hydrus_loam = [0.078, 0.43, 0.036, 1.56, 24.96] +# sp = vg.Parameters(hydrus_loam) +# peri.set_soil(sp) + +# #create the lookup table +# #t = time.time() +# #peri.create_lookup("results/hydrus_loam", sp) +# #elapsed_time = time.time() - t +# #print("Creating the lookup table took about ", elapsed_time) # + +# peri.open_lookup("results/hydrus_loam") + + +# #create the global lookup table +# #t = time.time() +# #peri.create_lookup_global("results/"+peri.water_filename, sp) +# #elapsed_time = time.time() - t +# #print("Creating the global lookup table took about ", elapsed_time) # + +# peri.open_global_lookup("results/"+peri.water_filename) + +# # create lookup tables for the solute flow +# Ds_=np.logspace(np.log10(1.0e0), np.log10(1.0e1), 2) +# #peri.create_integralDiffusion_lookup("results/hydrus_loam_ss_solutes", sp) +# #peri.create_integralconcentration_lookup("results/hydrus_loam_sr_solutes", Ds_, sp) + +# # open lookup tables for the solute flow +# peri.open_lookup_solutes("results/hydrus_loam_ss_solutes") +# peri.open_lookup_sr_solutes("results/hydrus_loam_sr_solutes") + + +# #numerically solve each root segment, this is the reference solution +# hsr = np.array([peri.soil_root_interface_(rx[i], sx[i], inner_kr[i], rho[i], peri.sp) for i in range(0, len(rx))]) +# tests.loc[:,"hsr"] = hsr +# print("norm of the root soil matrix potentials: ", LA.norm(hsr)) + +# #the standard implementation +# hsr1= peri.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) +# tests.loc[:,"hsr_lookup"] = hsr1 +# print("Norm of the difference to basic lookup table:", LA.norm(hsr-hsr1)) + +# #the alternative global implementation +# hsr3 = peri.soil_root_interface_potentials_table_global(rx, sx, inner_kr, rho) +# tests.loc[:,"hsr_global"] = hsr3 +# print("Norm of the difference to global lookup table:", LA.norm(hsr-hsr3)) + +# #waterflow = 2*3.14*np.multiply((hsr-rx),inner_kr)/r_root +# waterflow = 2*3.14*np.multiply((sx-rx),inner_kr)#/r_root +# Phi_root = np.array([vg.fast_mfp[sp](hsr[i]) for i in range(len(hsr))]) +# Phi_soil = np.array([vg.fast_mfp[sp](sx[i]) for i in range(len(sx))]) +# tests.loc[:,"waterflow"] = waterflow +# Ds = 1.0e1 + +# #base solutes +# tests.loc[:,"sol_c"] = c_sol +# tests.loc[:,"sol_U"] = np.array([Vmax[i]*c_sol[i]/(Km[i]+c_sol[i])*1e6 for i in range(len(c_sol))]) + +# # steady state solutes +# solutes_ss = peri.soil_root_solutes_ss_(Phi_root, Phi_soil, c_sol, Vmax, Km, Ds, waterflow, peri.sp) +# tests.loc[:,"sol_c_ss"] = solutes_ss +# tests.loc[:,"sol_U_ss"] = np.array([Vmax[i]*solutes_ss[i]/(Km[i]+solutes_ss[i])*1e6 for i in range(len(solutes_ss))]) + +# # steady rate solutes +# solutes_sr = peri.soil_root_solutes_sr_(Phi_root, Phi_soil, rho, c_sol, Vmax, Km, Ds, waterflow, peri.sp) +# tests.loc[:,"sol_c_sr"] = solutes_sr +# tests.loc[:,"sol_U_sr"] = np.array([Vmax[i]*solutes_sr[i]/(Km[i]+solutes_sr[i])*1e6 for i in range(len(solutes_sr))]) -print("All inputs:") -print(tests[inputs]) -print("All outputs:") -print(tests[outputs]) +# print("All inputs:") +# print(tests[inputs]) +# print("All outputs:") +# print(tests[outputs]) From 64b6d029641a2d9d23d8849dff55bf3b4dc19cf5 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Tue, 12 May 2026 21:01:48 +0200 Subject: [PATCH 11/41] dumux simulation for perirhizal test runs --- test/test_perirhizal.py | 357 +++++++++++++++------------------------- 1 file changed, 137 insertions(+), 220 deletions(-) diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 6590a8ed9..a3c198d42 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -12,7 +12,7 @@ from rosi.richards_flat import RichardsFlatWrapper as RichardsWrapper # Python part, macroscopic soil model #from richards import RichardsWrapper # Python part from rosi.richards_no_mpi import RichardsNoMPIWrapper # Python part of cylindrcial -from rosi.rosi_richardsnc_cyl import RichardsNCCylFoam as RichardsNC_cyl # C++ part (Dumux binding), macroscopic soil model +from rosi.rosi_richardsnc_cyl import RichardsNCCylFoam # C++ part (Dumux binding), macroscopic soil model #from rosi_richardsnc_cyl import RichardsNCCylFoam as RichardsNC_cyl # C++ part (Dumux binding) from rosi_richards22c import RichardsNCSPILU as RichardsNCSP #test @@ -20,6 +20,7 @@ import pandas as pd import numpy as np import time +import math # run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model @@ -28,7 +29,7 @@ # general parameters -max_time = 10 #d +max_time = 1 #d n_times = 100 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 1 # cm @@ -37,6 +38,8 @@ NC = 101 # number of spatial discretisations n_sp = NC - 1 +length = 3 #cm? + dt = max_time / n_times #space for the oupputs @@ -141,7 +144,7 @@ def run_perirhizal_test(): # determine some random parameters initial_waterpotential = -100 + np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? - initial_soluteconcentration = 1e-6*(1.0+np.random.rand()) #mol/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? + initial_soluteconcentration = 1e-6*(1.0+np.random.rand()) #g/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? print("initial_soluteconcentration",initial_soluteconcentration) # root conductivity and solute uptake parameters, it is chosen to be constant throughout the entire simulation time @@ -167,223 +170,134 @@ def run_perirhizal_test(): NC, base = lb) CC = np.array([(points[i] + points[i+1])/2 for i in range(len(points)-1)]) - + simtimes = np.linspace(max_time/n_times,max_time,n_times).tolist() # days #TODO remove # initialise the dumux model - print("test0") - #cyl = RichardsNoMPIWrapper(RichardsNCCylFoam()) - cyl = RichardsWrapper(RichardsNC_cyl()) - cyl = load_constants(cyl) - soilTexture = getSoilTextureAndShape(cyl, soilVG) - print("test005") - # cyl = RichardsNoMPIWrapper(RichardsNC_cyl()) - c2 = [cyl.mlFr * CC[i] for i in range(NC-1)] - #cyl = RichardsWrapper(RichardsCylFoam()) - #cyl.initialize() - cyl.createGrid1d(points) - initial=-10 - cyl.setHomogeneousIC(initial) # cm pressure head - #cyl.setICZ_solute(cyl.dumux_str([q for q in c2])) - cyl.setICZ_solute(str([q for q in c2])) - print("test006") - cyl.initialize() - - cyl.setVGParameters([soilVG]) - cyl.setOuterBC("fluxCyl", 0.) # [cm/day] - cyl.setInnerBC("fluxCyl", 0.) # [cm/day] - print("test007") - #cyl.initializeProblem() - cyl.maxDt = 3500/(3600*24) # soilModel.maxDt_1DS - #cyl.setParameter("Soil.mucilCAffectsW", "true") - cyl.setParameter("Problem.verbose", "0") - cyl.setParameter("Newton.Verbosity", "0") - cyl.initializeProblem(maxDt=cyl.maxDt ) - print("test008") - critP=-15000 - cyl.setCriticalPressure(critP) # cm pressure head - #cyls.append(cyl) - print("test0.1") - seg_length = 1 #cm - - if True: - cyl.setParameter("Newton.EnableResidualCriterion", "true") # sometimes helps, sometimes makes things worse - cyl.setParameter("Newton.EnableAbsoluteResidualCriterion", "true") - cyl.setParameter("Newton.SatisfyResidualAndShiftCriterion", "true") - cyl.setParameter("Newton.MaxRelativeShift", "1e-9")# reset value - #cyl.initialize(verbose = False) # No parameter file found. Continuing without parameter file. - #cyl.initialize() - print("test1") - #cyl.initializeProblem(maxDt=cyl.maxDt ) - cyl = load_constants(cyl) - soilTexture = getSoilTextureAndShape(cyl, soilVG) - - c2 = [cyl.mlFr * CC[i] for i in range(NC-1)] - - cyl.createGrid1d(points)# cm - cyl.doSoluteFlow = True - cyl.noAds = False - - #set soil parameters - cyl.solidDensity = soilTexture['solidDensity'] #[kg/m^3 solid] - cyl.solidMolarMass = soilTexture['solidMolarMass']# [kg/mol] - cyl.soil = soilTexture['soilVG'] - - cyl.setParameter( "Soil.MolarMass", str(cyl.solidMolarMass)) - cyl.setParameter( "Soil.solidDensity", str(cyl.solidDensity)) - #cyl.setVGParameters([cyl.soil]) - - minD=1.0e-12#minimal diffusion coefficient, I think this will be numerically good - #cyl.setParameter("1.Component.LiquidDiffusionCoefficient", str(DS_W/10000+minD)) #cm^2/s -> m^2/s - #todo: reactivate the diffusion coefficient - - #set michaelis menten uptake parameters - cyl.setParameter("RootSystem.Uptake.Vmax", str(Vmax)) - cyl.setParameter("RootSystem.Uptake.Km", str(Km)) - - #cyl = setSoilParam(cyl,paramSet) # here the soil parameters are set out of paramSet - #cyl = setBiochemParam(cyl,paramSet) # here the biochemical parameters are set out of paramSet - print("test2") - #cyl.initializeProblem(maxDt=cyl.maxDt ) - doCells = False - if doCells: - Cells = (cyl.getCellCenters_().reshape(-1)) - else: - Cells = [] - cyl.setParameter("Flux.UpwindWeight", "1") - cyl.setParameter("SpatialParams.Temperature","293.15") # todo: redefine at each time step - cyl.setParameter("Soil.BC.dzScaling", "1") - #cyl.setParameter( "Soil.css1Function", str(9)) # todo: can I activate this? - cyl.setParameter("Problem.verbose", "0") - cyl.setParameter("Problem.reactionExclusive", "0") - cyl.setParameter("Soil.CriticalPressure", str(-15000)) - cyl.seg_length = seg_length - cyl.setParameter("Problem.segLength", str(seg_length)) # cm - cyl.l = seg_length - cyl.setParameter( "Soil.Grid.Cells",str( NC-1)) # -1 to go from vertices to cell (dof) # I am unsure what this comment means - cyl.setParameter("Newton.MaxTimeStepDivisions", - str( 100) ) - cyl.setParameter("Newton.MaxSteps", - str( 100) ) - cyl.setParameter("Newton.EnableChop", "true") - print("test3") - #cyl.initializeProblem(maxDt=cyl.maxDt ) - #boundary conditions - #water - cyl.setParameter( "Soil.BC.Bot.Type", str(int(3))) - cyl.setParameter( "Soil.BC.Top.Type", str(int(3))) - cyl.setParameter( "Soil.BC.Bot.Value", str(0.0)) #will be prescribed at each timestep - cyl.setParameter( "Soil.BC.Top.Value", str(0.0)) - print("test3.1") - #solutes - cyl.setParameter( "Soil.BC.Bot.SType", str(int(3))) # TODO change this to 8 as Michaelis mnetne - cyl.setParameter( "Soil.BC.Top.SType", str(int(3))) - cyl.setParameter( "Soil.BC.Bot.CValue", str(0.0)) #Michaelis Menten parameters are set elsewhere - cyl.setParameter( "Soil.BC.Top.CValue", str(0.0)) - - print("test3.2") - #cyl.initializeProblem(maxDt=cyl.maxDt ) - - #initial conditions - cyl.setHomogeneousIC(initial_waterpotential) - #cyl.setParameter("Soil.IC.P", cyl.dumux_str(initial_waterpotential))# cm #this is where the initial matrix potential and with it the water content are set - #molar_fraction = initial_soluteconcentration / 62 * 18 /1 # / molar mass NO3 * molarMass Water - #print("molar_fraction",molar_fraction) - #cyl.setICZ_solute(c=[molar_fraction]) - theta = 0.3 #change this after testing - c2 = [cyl.mlFr * initial_soluteconcentration/cyl.mg_per_molC*cyl.masspercm3/theta for i in range(NC-1)] - #mlFr*paramSet['Soil.IC.C1']/cyl.mg_per_molC*masspercm3/theta - temp = np.array(c2)*initial_soluteconcentration - #temp2 = cyl.dumux_str(temp.tolist()) - temp2 = cyl.dumux_str(c2) - #cyl.initializeProblem(maxDt=cyl.maxDt ) - print("IC",temp2) - #cyl.initialize() - #cyl.initializeProblem(maxDt=cyl.maxDt ) - # - #cyl.initialize() - #cyl.setParameter("Soil.IC.C1",cyl.dumux_str([q for q in c2])) - #cyl.setParameter("Soil.IC.C", temp2)# cm #does this work without spatial resolution - #cyl.setParameter("Soil.IC.C", initial_soluteconcentration)# cm #does this work without spatial resolution - - print("test3.3") - cyl.initializeProblem(maxDt=cyl.maxDt ) + s = RichardsWrapper(RichardsNCCylFoam()) + + s.initialize() + s.createGrid1d(np.linspace(r_root, r_prhiz, NC)) # [cm] + s.setVGParameters([soilVG]) + + # theta = 0.378, benchmark is set be nearly fully saturated, so we don't care too much about the specific values + s.setHomogeneousIC(initial_waterpotential) # cm pressure head + + s.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")# + s.setBotBC("constantFluxCyl",0.0) # "noFlux")# + s.setParameter("Soil.BC.Top.SType", "3") # michaelisMenten=8 (SType = Solute Type) + s.setParameter("Soil.BC.Top.CValue", "0.0") # michaelisMenten=8 (SType = Solute Type) + s.setParameter("Soil.BC.Bot.SType", "3") # michaelisMenten=8 (SType = Solute Type) + s.setParameter("Soil.BC.Bot.CValue", "0.0") + + s.setParameter("Soil.IC.C", str(initial_soluteconcentration)) # g / cm3 # TODO specialised setter? + + s.setParameter("Component.MolarMass", "1.8e-2") + + s.setParameter("Component.LiquidDiffusionCoefficient", str(Ds)) # m2 s-1 + s.initializeProblem(maxDt = 0.01) + + cellVolumes = s.getCellSurfacesCyl() * length # cm3 + print(s) + + s.ddt = 1.e-3 # days + + simtimes.insert(0, 0) + dt_ = np.diff(simtimes) - if len(Cells) > 0:#in case we update a cylinder, to allow dumux to do interpolation - assert(len(c2)==len(Cells)) - CellsStr = cyl.dumux_str(Cells/100)#cm -> m #changed by Erik - cyl.setParameter("Soil.IC.Z",CellsStr)# m - if len(Cells)!= len(x): - print("Cells, x",Cells, x, len(Cells), len(x)) - raise Exception - - #j = 2 - for j in range( 1, cyl.numComp): - cyl.setParameter("Soil.IC.C"+str(j)+"Z",CellsStr) # m - if len(Cells)!= len( c2): - print("Cells, cAll[j-1]",Cells, c2, - len(Cells), len(c2), j) - raise Exception - print("test3.4") - cyl.maxDt = 3500/(3600*24) # soilModel.maxDt_1DS - #cyl.setParameter("Soil.mucilCAffectsW", "true") - cyl.setParameter("Problem.verbose", "0") - cyl.setParameter("Newton.Verbosity", "0") - cyl.initializeProblem(maxDt=cyl.maxDt ) + for r, dt in enumerate(dt_): + + + time = simtimes[r] + print('time',time) + + #if time >= 5: + # s.setSoluteTopBC([1], [0.]) + print("*****", "#", r, "external time step", dt, " d, simulation time", s.simTime, "d, internal time step", s.ddt, "d") + + Wvolbefore = cellVolumes * s.getWaterContent() # cm3 + Smassbefore = s.getSolution(1) * Wvolbefore # g - #cyl.setParameter("Problem.doNeffect","true") - - cyl.eps_regularization = 1e-14 - #cyl.setRegularisation(cyl.eps_regularization, cyl.eps_regularization) - #which one is the relative error? - #pcEps capillary pressure regularisation, krEps relative permeabiltiy regularisation - #default 1e-6, 1e-6 - cyl.setRegularisation(cyl.eps_regularization, cyl.eps_regularization) - - cyl.setCriticalPressure(-15000) # cm pressure head - #cyl = setInitialConditions(cyl,paramSet) - - # run the dumux model - #get water and solute next to the root - print("test3.5") - current_rs_potential = cyl.getSolutionHead() #todo rmeove - print(current_rs_potential[0]) - print("test3.6") - #current_rs_potential = current_rs_potential[0] - current_rs_potential = initial_waterpotential - print("test3.7") - current_rs_concentration = cyl.getSolution(1) * cyl.rhoWM # todo remove - print(current_rs_concentration) - print(current_rs_concentration[0]) - print("test3.8") - #current_rs_concentration = current_rs_concentration[0] - current_rs_concentration = initial_soluteconcentration - print("test4") - for i in range(n_times): - rx = rx_t(i*dt) #model root water uptake + rx = rx_t(i*dt) + current_rs_potential = s.getSolutionHead() + water_dumux[i,1:] = vg.water_content(current_rs_potential,peri.sp) + current_rs_potential = current_rs_potential[0] root_wateruptake = root_conductivity * (rx - current_rs_potential) water_dumux[i,0] = root_wateruptake - cyl.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) + #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) - #model root solute uptake + #model root solute uptake + #note: solutes are given in g + current_rs_concentration = s.getSolution(0) + solute_dumux[i,1:] = current_rs_concentration + current_rs_concentration = current_rs_concentration[0] root_soluteuptake = Vmax * current_rs_concentration * (Km + current_rs_concentration) - solute_dumux[i,0] = root_wateruptake - cyl.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) #todo set BC solute - print("test5") - cyl.solve(dt) + solute_dumux[i,0] = root_soluteuptake + #s.setParameter( "Soil.BC.Bot.SValue", str(root_soluteuptake)) - # save outputs of dumux - print("test6") - water_dumux[i,1:] = cyl.getWaterContent() - solute_dumux[i,1:] = cyl.getSolution(0) + s.solve(dt, saveInnerFluxes_ = True) + Wvolafter = cellVolumes*s.getWaterContent() # cm3 + Smassafter = s.getSolution(1) * Wvolafter # g - mean_water = np.mean(water_dumux[i,1:]) - mean_solutes = np.mean(solutes_dumux[i,1:]) + + rootSoilFluxes = s.getInnerFlow(0, length) * dt # cm3 + rootSoilFluxesS = s.getInnerFlow(1, length) * dt # g + soilSoilFluxes = s.getOuterFlow(0, length) * dt # cm3 + soilSoilFluxesS = s.getOuterFlow(1, length) * dt # g + # TODO: currently, setSource not properly implemented for richards and richards 2c + # so left out of the mass balance. + # scvSources = s.getSource(0) * cellVolumes * dt # cm3 + # scvSourcesS = s.getSource(1) * cellVolumes * dt # kg - current_rs_potential = cyl.getSolutionHead() - current_rs_potential = current_rs_potential[0] - current_rs_concentration = solute_dumux[i,1] + # #cyl.initializeProblem(maxDt=cyl.maxDt ) + # #boundary conditions + # #water + # cyl.setParameter( "Soil.BC.Bot.Type", str(int(3))) + # cyl.setParameter( "Soil.BC.Top.Type", str(int(3))) + # cyl.setParameter( "Soil.BC.Bot.Value", str(0.0)) #will be prescribed at each timestep + # cyl.setParameter( "Soil.BC.Top.Value", str(0.0)) + + # #solutes + # s.setParameter( "Soil.BC.Bot.SType", str(int(3))) # TODO change this to 8 as Michaelis mnetne + # s.setParameter( "Soil.BC.Top.SType", str(int(3))) + # s.setParameter( "Soil.BC.Bot.CValue", str(0.0)) #Michaelis Menten parameters are set elsewhere + # s.setParameter( "Soil.BC.Top.CValue", str(0.0)) + # print("test") + + # # run the dumux model + # #get water and solute next to the root + # current_rs_potential = s.getSolutionHead()[0] #todo rmeove + # print(current_rs_potential) + # current_rs_concentration = s.getSolution(1)[0] * s.rhoWM # todo remove + # print(current_rs_concentration[0]) + # for i in range(n_times): + # rx = rx_t(i*dt) + # #model root water uptake + # root_wateruptake = root_conductivity * (rx - current_rs_potential) + # water_dumux[i,0] = root_wateruptake + # #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) + # #s.setSource( 0, str(root_wateruptake)) #TODO activat ehtis + # print("test") + # #model root solute uptake + # root_soluteuptake = Vmax * current_rs_concentration * (Km + current_rs_concentration) + # solute_dumux[i,0] = root_wateruptake + # #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) #todo set BC solute + # print("test5") + # s.solve(dt) + + # # save outputs of dumux + # print("test6") + # water_dumux[i,1:] = cyl.getWaterContent() + # solute_dumux[i,1:] = cyl.getSolution(0) + + # mean_water = np.mean(water_dumux[i,1:]) + # mean_solutes = np.mean(solutes_dumux[i,1:]) + + + # current_rs_potential = cyl.getSolutionHead() + # current_rs_potential = current_rs_potential[0] + # current_rs_concentration = solute_dumux[i,1] @@ -405,8 +319,8 @@ def run_perirhizal_test(): h_sr = peri.soil_root_interface_(rx, sx, inner_kr, rho, peri.sp) #compute both matrix flux potentials: - Phi_root = vg.fast_mfp[sp](h_sr) - Phi_soil = vg.fast_mfp[sp](s_x) + Phi_root = np.array(vg.fast_mfp[sp](h_sr)) + Phi_soil = np.array(vg.fast_mfp[sp](sx)) #compute the spatial watercontents Phi_A, Phi_C = peri.determine_mfp_function(Phi_root, Phi_soil, rho) @@ -414,31 +328,34 @@ def run_perirhizal_test(): Phi_out = Phi_A+Phi_C for j in range(NC-1): - s = CC[j] / r_prhiz - Phi = Phi_A*(s**2-np.log(s**2))+Phi_C - water_sr[i,j] = vg.water_content(vg.fast_imfp[sp](),peri.sp) - + r_rel = CC[j] / r_prhiz + Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C + water_sr[i,j] = vg.water_content(vg.fast_imfp[sp](Phi),peri.sp) # run the alpha omega model on solute flow (both steady state and steady rate) waterflow = root_wateruptake - result_solutes_ss = peri.soil_root_solutes_ss_(Phi_out, Phi_soil, total_solute, Vmax, Km, Ds, waterflow, peri.sp) - result_solutes_sr = peri.soil_root_solutes_sr_(Phi_root, Phi_soil, rho, total_solute, Vmax, Km, Ds, waterflow, peri.sp) + result_solutes_ss = peri.soil_root_solutes_ss_([Phi_out], [Phi_soil], [total_solute], [Vmax], [Km], Ds, [waterflow], peri.sp) + result_solutes_sr = peri.soil_root_solutes_sr_([Phi_root], [Phi_soil], [rho], [total_solute], [Vmax], [Km], Ds, [waterflow], peri.sp) + result_solutes_ss = result_solutes_ss[0] + result_solutes_sr = result_solutes_sr[0] solute_ss[i,1] = result_solutes_ss solute_ss[i,0] = Vmax * result_solutes_ss / (Km + result_solutes_ss) solute_sr[i,1] = result_solutes_sr solute_sr[i,0] = Vmax * result_solutes_sr / (Km + result_solutes_sr) - F0 = self.lookup_table_solutes((Phi_root[i],0)) # for the ratio of concentration next to the root to somewhere in the perirhizal zone + #F0 = peri.lookup_table_solutes((Phi_root,0)) # for the ratio of concentration next to the root to somewhere in the perirhizal zone + F0 = peri.integral_overDiffusion_(Phi,peri.sp) D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) for j in range(NC-2): - s = CC[j+1] / r_prhiz - Phi_current = Phi_A*(s**2-np.log(s**2))+Phi_C - F = self.lookup_table_solutes((Phi_soil[i],0))-F0 + r_rel = CC[j+1] / r_prhiz + Phi_current = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C + #F = peri.lookup_table_solutes((Phi_current,0))-F0 + F = peri.integral_overDiffusion_(Phi_current,peri.sp)-F0 F_tilde=math.exp(D_tilde*F) solute_ss[i,1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[i,0] / waterflow solute_sr[i,1] = result_solutes_sr * F_tilde + (1-F_tilde) * solute_sr[i,0] / waterflow - + print("test4") return water_dumux, solute_dumux, solute_ss, water_sr, solute_sr From de83ff034d61b3ddba9ae03cf77e8e2712bd7606 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Wed, 13 May 2026 16:58:18 +0200 Subject: [PATCH 12/41] uptake of solutes and water --- test/test_perirhizal.py | 93 +++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index a3c198d42..419f84d26 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -21,16 +21,18 @@ import numpy as np import time import math +import matplotlib.pyplot as plt +from plantbox.visualisation import figure_style # run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model -n_tests = 10 #try everything here for this many random parameter sets -do_computation = True #should the computation be run or take the data from a saved file +n_tests = 1 #try everything here for this many random parameter sets +do_computation = False #should the computation be run or take the data from a saved file # general parameters max_time = 1 #d -n_times = 100 # number of time intervals +n_times = 10 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 1 # cm r_root = 0.02 # cm @@ -51,6 +53,11 @@ soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil +lb = 0.5 +points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), + NC, base = lb) +CC = np.array([(points[i] + points[i+1])/2 for i in range(len(points)-1)]) + def load_constants(cyl): cyl.molarMassC = 12.011 @@ -165,10 +172,7 @@ def run_perirhizal_test(): peri.set_soil(sp) #no lookup tables are used here as there arent many simulations - lb = 0.5 - points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), - NC, base = lb) - CC = np.array([(points[i] + points[i+1])/2 for i in range(len(points)-1)]) + simtimes = np.linspace(max_time/n_times,max_time,n_times).tolist() # days #TODO remove @@ -218,22 +222,28 @@ def run_perirhizal_test(): Smassbefore = s.getSolution(1) * Wvolbefore # g #model root water uptake - rx = rx_t(i*dt) + rx = rx_t(r*dt) current_rs_potential = s.getSolutionHead() - water_dumux[i,1:] = vg.water_content(current_rs_potential,peri.sp) + water_dumux[r,1:] = vg.water_content(current_rs_potential,peri.sp) current_rs_potential = current_rs_potential[0] root_wateruptake = root_conductivity * (rx - current_rs_potential) - water_dumux[i,0] = root_wateruptake + water_dumux[r,0] = root_wateruptake + print(water_dumux) #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) + #model root solute uptake #note: solutes are given in g current_rs_concentration = s.getSolution(0) - solute_dumux[i,1:] = current_rs_concentration + solute_dumux[r,1:] = current_rs_concentration current_rs_concentration = current_rs_concentration[0] root_soluteuptake = Vmax * current_rs_concentration * (Km + current_rs_concentration) - solute_dumux[i,0] = root_soluteuptake + solute_dumux[r,0] = root_soluteuptake #s.setParameter( "Soil.BC.Bot.SValue", str(root_soluteuptake)) + #s.initializeProblem(maxDt = 0.01) + + s.setSource({0: root_wateruptake * length}) + s.setSource({0: root_soluteuptake * length}, eq_idx = 1) s.solve(dt, saveInnerFluxes_ = True) Wvolafter = cellVolumes*s.getWaterContent() # cm3 @@ -307,8 +317,8 @@ def run_perirhizal_test(): total_water = 0 total_solute = 0 for j in range(NC-1): - total_water = total_water + water_dumux[i,j+1]*(points[j+1]**2 - points[j]**2) - total_solute = total_solute + water_dumux[i,j+1]*solute_dumux[i,j+1]*(points[j+1]**2 - points[j]**2) + total_water = total_water + water_dumux[r,j+1]*(points[j+1]**2 - points[j]**2) + total_solute = total_solute + water_dumux[r,j+1]*solute_dumux[r,j+1]*(points[j+1]**2 - points[j]**2) mean_water = total_water / (points[NC-1]**2 - points[0]**2) total_solute = total_solute / (points[NC-1]**2 - points[0]**2) @@ -330,33 +340,31 @@ def run_perirhizal_test(): for j in range(NC-1): r_rel = CC[j] / r_prhiz Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C - water_sr[i,j] = vg.water_content(vg.fast_imfp[sp](Phi),peri.sp) + water_sr[r,j+1] = vg.water_content(vg.fast_imfp[sp](Phi),peri.sp) # run the alpha omega model on solute flow (both steady state and steady rate) waterflow = root_wateruptake - result_solutes_ss = peri.soil_root_solutes_ss_([Phi_out], [Phi_soil], [total_solute], [Vmax], [Km], Ds, [waterflow], peri.sp) - result_solutes_sr = peri.soil_root_solutes_sr_([Phi_root], [Phi_soil], [rho], [total_solute], [Vmax], [Km], Ds, [waterflow], peri.sp) + result_solutes_ss = peri.soil_root_solutes_ss_([Phi_out], [Phi_soil], [total_solute / total_water], [Vmax], [Km], Ds, [waterflow], peri.sp) + result_solutes_sr = peri.soil_root_solutes_sr_([Phi_root], [Phi_soil], [rho], [total_solute / total_water], [Vmax], [Km], Ds, [waterflow], peri.sp) result_solutes_ss = result_solutes_ss[0] result_solutes_sr = result_solutes_sr[0] - solute_ss[i,1] = result_solutes_ss - solute_ss[i,0] = Vmax * result_solutes_ss / (Km + result_solutes_ss) - solute_sr[i,1] = result_solutes_sr - solute_sr[i,0] = Vmax * result_solutes_sr / (Km + result_solutes_sr) + solute_ss[r,1] = result_solutes_ss + solute_ss[r,0] = Vmax * result_solutes_ss / (Km + result_solutes_ss) + solute_sr[r,1] = result_solutes_sr + solute_sr[r,0] = Vmax * result_solutes_sr / (Km + result_solutes_sr) #F0 = peri.lookup_table_solutes((Phi_root,0)) # for the ratio of concentration next to the root to somewhere in the perirhizal zone F0 = peri.integral_overDiffusion_(Phi,peri.sp) D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) - for j in range(NC-2): - r_rel = CC[j+1] / r_prhiz + for j in range(NC-1): + r_rel = CC[j] / r_prhiz Phi_current = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C #F = peri.lookup_table_solutes((Phi_current,0))-F0 F = peri.integral_overDiffusion_(Phi_current,peri.sp)-F0 F_tilde=math.exp(D_tilde*F) - solute_ss[i,1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[i,0] / waterflow - solute_sr[i,1] = result_solutes_sr * F_tilde + (1-F_tilde) * solute_sr[i,0] / waterflow - print("test4") - + solute_ss[r,j+1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[r,0] / waterflow + solute_sr[r,j+1] = result_solutes_sr * F_tilde + (1-F_tilde) * solute_sr[r,0] / waterflow return water_dumux, solute_dumux, solute_ss, water_sr, solute_sr if do_computation: @@ -381,29 +389,32 @@ def run_perirhizal_test(): # compare both for the differint means of water / solute content -run = 5 -timestep = 10 +run = 0 +timestep = 5 #plot water and nitrogen in that one voxel fig, ax1 = figure_style.subplots12(1, 1) ax2 = ax1.twinx() -water_dumux = watercontent_dumux[run, timestep, :] -water_perirhizal = watercontent_steadyrate[run, timestep, :] -solute_dumux = soluteconcentration_dumux[run, timestep, :] -solute_steadystate = soluteconcentration_steadystate[run, timestep, :] -solute_steadyrate = soluteconcentration_steadyrate[run, timestep, :] +water_dumux = watercontent_dumux[run, timestep, 1:] +water_perirhizal = watercontent_steadyrate[run, timestep, 1:] +solute_dumux = soluteconcentration_dumux[run, timestep, 1:] +solute_steadystate = soluteconcentration_steadystate[run, timestep, 1:] +solute_steadyrate = soluteconcentration_steadyrate[run, timestep, 1:] + linestyle_dumux = "solid" linestyle_steadystate = "dotted" linestyle_steadyrate = "dashed" -ax1.plot(times, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") -ax1.plot(times, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") -ax2.plot(times, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") -ax2.plot(times, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") -ax2.plot(times, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - -ax1.set_xlabel("time (day)") +#print(water_dumux) +#print(CC) +ax1.plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") +ax1.plot(CC, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") +ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") +ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") +ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + +ax1.set_xlabel("distance root [cm]") ax1.set_ylabel("water") ax2.set_ylabel("nitrogen concentration") ax1.legend(["watercontent cm3/cm3"], loc="upper left") From e035069ecddcd1793a6e9e606f5313ab3458871a Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Mon, 18 May 2026 08:22:55 +0200 Subject: [PATCH 13/41] water perirhizal model test dumux --- src/functional/Perirhizal.py | 3 +- test/test_perirhizal.py | 97 +++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index bc098e91b..150ca3511 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -242,7 +242,7 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf return rsc - def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, waterflow, sp, approximate_rho = True): + def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, waterflow, sp, approximate_rho = False): """ finds solute concentration at the soil root interface for all segments assuming steady rate largely reuses approach of the steady state case, it just needs to compute the factor between mean concentration and the concentration at the outer boundary @@ -353,6 +353,7 @@ def integral_overconcentration_(self, Ds, Phi_root, Phi_soil, rho, sp): integral_R_sr, _ = integrate.quad(integral_fun, 1/(rho*rho), 1.0**2) R_sr = integral_R_sr / ((1.0**2-1/(rho*rho))) #print(integral_fun(1/(rho*rho)),integral_fun(0.53**2),integral_fun(1.0**2),integral_R_sr, rho) + print("R_sr",R_sr) return R_sr def determine_mfp_function(self, Phi_root, Phi_soil, rho): diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 419f84d26..419379fbf 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -27,20 +27,22 @@ # run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model n_tests = 1 #try everything here for this many random parameter sets -do_computation = False #should the computation be run or take the data from a saved file +do_computation = True #should the computation be run or take the data from a saved file # general parameters max_time = 1 #d -n_times = 10 # number of time intervals +n_times = 100 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 1 # cm r_root = 0.02 # cm + + rho = r_prhiz / r_root NC = 101 # number of spatial discretisations n_sp = NC - 1 -length = 3 #cm? +length = 1 #cm? dt = max_time / n_times @@ -55,7 +57,7 @@ lb = 0.5 points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), - NC, base = lb) + NC, base = lb) CC = np.array([(points[i] + points[i+1])/2 for i in range(len(points)-1)]) def load_constants(cyl): @@ -150,21 +152,21 @@ def run_perirhizal_test(): mean_solutes = np.zeros((n_times)) # determine some random parameters - initial_waterpotential = -100 + np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? + initial_waterpotential = -3000 + np.random.rand() * 200 #cm3/cm3 #or choose an initial pressure head? initial_soluteconcentration = 1e-6*(1.0+np.random.rand()) #g/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? print("initial_soluteconcentration",initial_soluteconcentration) # root conductivity and solute uptake parameters, it is chosen to be constant throughout the entire simulation time root_conductivity = 1e-4 #1/d - inner_kr = root_conductivity * r_root - Vmax = 1.0e-10 #mol/d - Km = 1.0e-9 #mol/cm3 + inner_kr = root_conductivity * r_root * 2 * 3.14 + Vmax = 4.0e-11 * 62 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * (g/mol) * cm * cm * (s/d) -> g / d + Km = 1.5e-7 * 62 #mol/cm3 -> g/cm3 DS_W = 1.902e-5 #cm2/s - Ds = DS_W * 3600 * 24 #cm2/d + Ds = DS_W * 3600 * 24 / 100#cm2/d # the xylem matrix potential varies over time (keep it low so that there is little to no outflow of water) - rx_t = lambda t : -500+200*np.sin(t) #cm + rx_t = lambda t : -7000+200*np.sin(t) #cm # load the perirhizal model peri = PerirhizalPython() @@ -180,7 +182,7 @@ def run_perirhizal_test(): s = RichardsWrapper(RichardsNCCylFoam()) s.initialize() - s.createGrid1d(np.linspace(r_root, r_prhiz, NC)) # [cm] + s.createGrid1d(np.linspace(r_root, r_prhiz, NC), length = length/100) # [cm] s.setVGParameters([soilVG]) # theta = 0.378, benchmark is set be nearly fully saturated, so we don't care too much about the specific values @@ -195,15 +197,17 @@ def run_perirhizal_test(): s.setParameter("Soil.IC.C", str(initial_soluteconcentration)) # g / cm3 # TODO specialised setter? - s.setParameter("Component.MolarMass", "1.8e-2") + #s.setParameter("Component.MolarMass", "1.8e-2") + s.setParameter("Component.MolarMass", "62.0") s.setParameter("Component.LiquidDiffusionCoefficient", str(Ds)) # m2 s-1 s.initializeProblem(maxDt = 0.01) cellVolumes = s.getCellSurfacesCyl() * length # cm3 + print("cellVolumes",cellVolumes) print(s) - s.ddt = 1.e-3 # days + s.ddt = 1.e-4 # days simtimes.insert(0, 0) dt_ = np.diff(simtimes) @@ -226,7 +230,7 @@ def run_perirhizal_test(): current_rs_potential = s.getSolutionHead() water_dumux[r,1:] = vg.water_content(current_rs_potential,peri.sp) current_rs_potential = current_rs_potential[0] - root_wateruptake = root_conductivity * (rx - current_rs_potential) + root_wateruptake = inner_kr * (rx - current_rs_potential) water_dumux[r,0] = root_wateruptake print(water_dumux) #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) @@ -234,11 +238,12 @@ def run_perirhizal_test(): #model root solute uptake #note: solutes are given in g - current_rs_concentration = s.getSolution(0) + current_rs_concentration = s.getSolution(1) solute_dumux[r,1:] = current_rs_concentration current_rs_concentration = current_rs_concentration[0] - root_soluteuptake = Vmax * current_rs_concentration * (Km + current_rs_concentration) + root_soluteuptake = - Vmax * current_rs_concentration / (Km + current_rs_concentration) solute_dumux[r,0] = root_soluteuptake + print(solute_dumux) #s.setParameter( "Soil.BC.Bot.SValue", str(root_soluteuptake)) #s.initializeProblem(maxDt = 0.01) @@ -321,6 +326,7 @@ def run_perirhizal_test(): total_solute = total_solute + water_dumux[r,j+1]*solute_dumux[r,j+1]*(points[j+1]**2 - points[j]**2) mean_water = total_water / (points[NC-1]**2 - points[0]**2) total_solute = total_solute / (points[NC-1]**2 - points[0]**2) + mean_solute = total_solute / mean_water #translate the mean water content to a mean matrix potential sx = vg.pressure_head(mean_water, peri.sp) @@ -344,8 +350,8 @@ def run_perirhizal_test(): # run the alpha omega model on solute flow (both steady state and steady rate) waterflow = root_wateruptake - result_solutes_ss = peri.soil_root_solutes_ss_([Phi_out], [Phi_soil], [total_solute / total_water], [Vmax], [Km], Ds, [waterflow], peri.sp) - result_solutes_sr = peri.soil_root_solutes_sr_([Phi_root], [Phi_soil], [rho], [total_solute / total_water], [Vmax], [Km], Ds, [waterflow], peri.sp) + result_solutes_ss = peri.soil_root_solutes_ss_([Phi_root], [Phi_out], [mean_solute], [Vmax], [Km], Ds, [waterflow], peri.sp) + result_solutes_sr = peri.soil_root_solutes_sr_([Phi_root], [Phi_soil], [rho], [mean_solute], [Vmax], [Km], Ds, [waterflow], peri.sp) result_solutes_ss = result_solutes_ss[0] result_solutes_sr = result_solutes_sr[0] @@ -355,7 +361,7 @@ def run_perirhizal_test(): solute_sr[r,0] = Vmax * result_solutes_sr / (Km + result_solutes_sr) #F0 = peri.lookup_table_solutes((Phi_root,0)) # for the ratio of concentration next to the root to somewhere in the perirhizal zone - F0 = peri.integral_overDiffusion_(Phi,peri.sp) + F0 = peri.integral_overDiffusion_(Phi_root,peri.sp) D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) for j in range(NC-1): r_rel = CC[j] / r_prhiz @@ -390,17 +396,17 @@ def run_perirhizal_test(): # compare both for the differint means of water / solute content run = 0 -timestep = 5 +timestep = [1,3,5,7,9] +for i in range(5): + timestep[i] = int(n_times * timestep[i] / 10) #plot water and nitrogen in that one voxel -fig, ax1 = figure_style.subplots12(1, 1) -ax2 = ax1.twinx() +#fig, ax1 = figure_style.subplots12(1, 5) +fig, ax1 = figure_style.subplots12(nrows=5, ncols=1) + + + -water_dumux = watercontent_dumux[run, timestep, 1:] -water_perirhizal = watercontent_steadyrate[run, timestep, 1:] -solute_dumux = soluteconcentration_dumux[run, timestep, 1:] -solute_steadystate = soluteconcentration_steadystate[run, timestep, 1:] -solute_steadyrate = soluteconcentration_steadyrate[run, timestep, 1:] linestyle_dumux = "solid" linestyle_steadystate = "dotted" @@ -408,20 +414,29 @@ def run_perirhizal_test(): #print(water_dumux) #print(CC) -ax1.plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") -ax1.plot(CC, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") -ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") -ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") -ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - -ax1.set_xlabel("distance root [cm]") -ax1.set_ylabel("water") -ax2.set_ylabel("nitrogen concentration") -ax1.legend(["watercontent cm3/cm3"], loc="upper left") -ax2.legend(["nitrogen concentration mol/cm3"], loc="upper right") - -ax1.legend(loc="upper left") -ax2.legend(loc="upper right") +for i in range(5): + ax2 = ax1[i].twinx() + + water_dumux = watercontent_dumux[run, timestep[i], 1:] + water_perirhizal = watercontent_steadyrate[run, timestep[i], 1:] + solute_dumux = soluteconcentration_dumux[run, timestep[i], 1:] + solute_steadystate = soluteconcentration_steadystate[run, timestep[i], 1:] + solute_steadyrate = soluteconcentration_steadyrate[run, timestep[i], 1:] + + ax1[i].plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") + ax1[i].plot(CC, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") + ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") + ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") + ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + + ax1[i].set_xlabel("distance root [cm]") + ax1[i].set_ylabel("water") + ax2.set_ylabel("nitrogen concentration") + ax1[i].legend(["watercontent cm3/cm3"], loc="upper left") + ax2.legend(["nitrogen concentration mol/cm3"], loc="upper right") + + ax1[i].legend(loc="upper left") + ax2.legend(loc="upper right") #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) plt.show() From e0c46e4a4232e7d1643d75f58629fe7387d5a4b4 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Tue, 19 May 2026 08:56:29 +0200 Subject: [PATCH 14/41] update perirhizal test file --- src/functional/Perirhizal.py | 20 +++++++------ test/test_perirhizal.py | 58 ++++++++++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 150ca3511..bf81f1399 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -220,7 +220,7 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf rsc = np.zeros(n_segments) F = np.zeros(n_segments) #F is a helper values - F_tilde = np.zeros(n_segments) + F_tilde_inv = np.zeros(n_segments) if self.lookup_table_solutes: F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, len(c_bulk))] @@ -232,9 +232,10 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf #solve quadratic eqation # TODO: Link to publication for i in range(n_segments): - F_tilde[i]=math.exp(D_tilde*F[i]) - a1=c_bulk[i]/F_tilde[i] - a2=(F_tilde[i]-1)/(F_tilde[i]*waterflow[i]) + print("Dtilde",D_tilde,"F",F[i]) + F_tilde_inv[i]=math.exp(-D_tilde*F[i]) + a1=c_bulk[i]*F_tilde_inv[i] + a2=(1-F_tilde_inv[i])/(waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) @@ -265,7 +266,7 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w rsc = np.zeros(n_segments) F = np.zeros(n_segments) #F is a helper value - F_tilde = np.zeros(n_segments) + F_tilde_inv = np.zeros(n_segments) R_sr = np.zeros(n_segments) watercontent = np.zeros(n_segments) @@ -295,13 +296,14 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w #reuse steady state case #solve quadratic eqation # TODO: Link to publication for i in range(n_segments): - F_tilde[i]=math.exp(C_d*F[i]) - a1=(c_bulk[i]*watercontent[i])/(F_tilde[i]*R_sr[i]) - a2=(F_tilde[i]*R_sr[i]-watercontent[i])/(F_tilde[i]*R_sr[i]*waterflow[i]) + print("C_d",C_d,"F",F[i]) + F_tilde_inv[i]=math.exp(-C_d*F[i]) + a1=(c_bulk[i]*watercontent[i])/(R_sr[i])*F_tilde_inv[i] + a2=(1-watercontent[i]/R_sr[i]*F_tilde_inv[i])/waterflow[i] p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 if q>0: - print("q>0",Km[i],a1,F_tilde[i],R_sr[i],c_bulk[i],watercontent[i]) + print("q>0",Km[i],a1,F_tilde_inv[i],R_sr[i],c_bulk[i],watercontent[i]) q=0 rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 419379fbf..dc3eb4b6b 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -31,10 +31,10 @@ # general parameters -max_time = 1 #d -n_times = 100 # number of time intervals +max_time = 3 #d +n_times = 300 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] -r_prhiz = 1 # cm +r_prhiz = 0.3 # cm r_root = 0.02 # cm @@ -53,7 +53,7 @@ watercontent_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) soluteconcentration_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) -soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil +soilVG = [0.078, 0.43, 0.036, 1.56, 24.96*10] # hydrus loam soil #test:conductivity times 10 TODO remove test lb = 0.5 points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), @@ -152,18 +152,20 @@ def run_perirhizal_test(): mean_solutes = np.zeros((n_times)) # determine some random parameters - initial_waterpotential = -3000 + np.random.rand() * 200 #cm3/cm3 #or choose an initial pressure head? - initial_soluteconcentration = 1e-6*(1.0+np.random.rand()) #g/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? + initial_waterpotential = -3000 + np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? + initial_soluteconcentration = 2e-5*(1.0+np.random.rand()) #g/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? print("initial_soluteconcentration",initial_soluteconcentration) # root conductivity and solute uptake parameters, it is chosen to be constant throughout the entire simulation time root_conductivity = 1e-4 #1/d inner_kr = root_conductivity * r_root * 2 * 3.14 - Vmax = 4.0e-11 * 62 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * (g/mol) * cm * cm * (s/d) -> g / d - Km = 1.5e-7 * 62 #mol/cm3 -> g/cm3 + Vmax = 4.0e-11 * 62 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * (g/mol) * cm * cm * (s/d) -> g / d #TODO: remove test multiplication by 10 + Km = 1.5e-7 * 62 #mol/cm3 -> g/cm3 - DS_W = 1.902e-5 #cm2/s - Ds = DS_W * 3600 * 24 / 100#cm2/d + #DS_W = 1.902e-5 #cm2/s + #Ds = DS_W / 10000#m2/s + Ds = 1.902e-5 #cm2/s + Ds = Ds * 24 * 3600 #cm2/d #TODO: remove the division by 3 after testing # the xylem matrix potential varies over time (keep it low so that there is little to no outflow of water) rx_t = lambda t : -7000+200*np.sin(t) #cm @@ -200,7 +202,7 @@ def run_perirhizal_test(): #s.setParameter("Component.MolarMass", "1.8e-2") s.setParameter("Component.MolarMass", "62.0") - s.setParameter("Component.LiquidDiffusionCoefficient", str(Ds)) # m2 s-1 + s.setParameter("Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600))) # m2 s-1 s.initializeProblem(maxDt = 0.01) cellVolumes = s.getCellSurfacesCyl() * length # cm3 @@ -354,6 +356,7 @@ def run_perirhizal_test(): result_solutes_sr = peri.soil_root_solutes_sr_([Phi_root], [Phi_soil], [rho], [mean_solute], [Vmax], [Km], Ds, [waterflow], peri.sp) result_solutes_ss = result_solutes_ss[0] result_solutes_sr = result_solutes_sr[0] + print("steadystate", result_solutes_ss, "steadyrate", result_solutes_sr) solute_ss[r,1] = result_solutes_ss solute_ss[r,0] = Vmax * result_solutes_ss / (Km + result_solutes_ss) @@ -364,10 +367,12 @@ def run_perirhizal_test(): F0 = peri.integral_overDiffusion_(Phi_root,peri.sp) D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) for j in range(NC-1): + r_rel = CC[j] / r_prhiz Phi_current = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C #F = peri.lookup_table_solutes((Phi_current,0))-F0 F = peri.integral_overDiffusion_(Phi_current,peri.sp)-F0 + #print("Ds", Ds, "Dtilde",D_tilde,"F",F,"Dtilde*F",D_tilde*F) F_tilde=math.exp(D_tilde*F) solute_ss[r,j+1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[r,0] / waterflow solute_sr[r,j+1] = result_solutes_sr * F_tilde + (1-F_tilde) * solute_sr[r,0] / waterflow @@ -431,7 +436,7 @@ def run_perirhizal_test(): ax1[i].set_xlabel("distance root [cm]") ax1[i].set_ylabel("water") - ax2.set_ylabel("nitrogen concentration") + ax2.set_ylabel("nitrogen") ax1[i].legend(["watercontent cm3/cm3"], loc="upper left") ax2.legend(["nitrogen concentration mol/cm3"], loc="upper right") @@ -440,7 +445,34 @@ def run_perirhizal_test(): #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) plt.show() - +# for i in range(5): + # for j in range(2): + # ax2 = ax1[i,j].twinx() + + # water_dumux = watercontent_dumux[run, timestep[i], 1:] + # water_perirhizal = watercontent_steadyrate[run, timestep[i], 1:] + # solute_dumux = soluteconcentration_dumux[run, timestep[i], 1:] + # solute_steadystate = soluteconcentration_steadystate[run, timestep[i], 1:] + # solute_steadyrate = soluteconcentration_steadyrate[run, timestep[i], 1:] + + # ax1[i,j].plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") + # ax1[i,j].plot(CC, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") + # ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") + # if j==0: + # ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") + # else: + # ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + + # ax1[i,j].set_xlabel("distance root [cm]") + # ax1[i,j].set_ylabel("water") + # ax2.set_ylabel("nitrogen concentration") + # ax1[i,j].legend(["watercontent cm3/cm3"], loc="upper left") + # ax2.legend(["nitrogen concentration mol/cm3"], loc="upper right") + + # ax1[i,j].legend(loc="upper left") + # ax2.legend(loc="upper right") +# #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +# plt.show() # #---old stuff below From 85440c1552df021bc6e6d3bea0ffeec75ac0a0ac Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Wed, 27 May 2026 07:32:10 +0200 Subject: [PATCH 15/41] safe changes 260527 --- src/functional/Perirhizal.py | 4 +- test/test_perirhizal.py | 410 ++++++++++------------------------- 2 files changed, 122 insertions(+), 292 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index bf81f1399..ecde6f757 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -151,7 +151,7 @@ def soil_root_interface_(rx, sx, inner_kr, rho, sp): def soil_root_interface_simp_(self, inner_kr_b, base_mfp, sp): """ finds matric potential at the soil root interface for as single segment - gives (numerically) the same "results as soil_root_interface_", but is much simpler to create a lookup table for + gives (numerically) the same results as "soil_root_interface_", but is much simpler to create a lookup table for rx xylem matric potential [cm] sx bulk soil matric potential [cm] @@ -186,7 +186,7 @@ def soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m): base_mfp: -((inner_kr * alpha)/(alpha_0 * Ks * b) * rx + vg.fast_mfp[sp_base*(alpha_0/alpha)](sx)) vg_m: van genuchten parameter m - (sp_base is the van genuchten parameter set of Ks = 1 and alpha = alpha_0, m is an input) + (sp_base is the van genuchten parameter set of Ks = 1 and alpha = alpha_0, m is an input, theta_r and theta_s are not important for the steady rate model computaiton right now) """ diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index dc3eb4b6b..378db0061 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -14,7 +14,7 @@ from rosi.richards_no_mpi import RichardsNoMPIWrapper # Python part of cylindrcial from rosi.rosi_richardsnc_cyl import RichardsNCCylFoam # C++ part (Dumux binding), macroscopic soil model #from rosi_richardsnc_cyl import RichardsNCCylFoam as RichardsNC_cyl # C++ part (Dumux binding) -from rosi_richards22c import RichardsNCSPILU as RichardsNCSP #test +#from rosi_richards22c import RichardsNCSPILU as RichardsNCSP #test import Perirhizal import pandas as pd @@ -31,15 +31,15 @@ # general parameters -max_time = 3 #d -n_times = 300 # number of time intervals +max_time = 40 #d +n_times = 400 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] -r_prhiz = 0.3 # cm +r_prhiz = 0.6 # cm r_root = 0.02 # cm rho = r_prhiz / r_root -NC = 101 # number of spatial discretisations +NC = 41 # number of spatial discretisations n_sp = NC - 1 length = 1 #cm? @@ -51,124 +51,60 @@ soluteconcentration_dumux = np.zeros((n_tests, n_times, n_sp+1)) soluteconcentration_steadystate = np.zeros((n_tests, n_times, n_sp+1)) watercontent_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) +watercontent_steadyrate2 = np.zeros((n_tests, n_times, n_sp+1)) soluteconcentration_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) -soilVG = [0.078, 0.43, 0.036, 1.56, 24.96*10] # hydrus loam soil #test:conductivity times 10 TODO remove test +matrixpotential_perirhizal2 = np.zeros((n_tests, n_times, n_sp+1)) + +soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil + +soilVG = [0.08, 0.43, 0.04, 1.6, 50] #loam from benchmark #da war am Ende noch eine 0.5, keine Ahnung warum, vermutlich eine vorherige Programmversion lb = 0.5 points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), NC, base = lb) CC = np.array([(points[i] + points[i+1])/2 for i in range(len(points)-1)]) -def load_constants(cyl): - - cyl.molarMassC = 12.011 - cyl.molarMassN = 14.0 - cyl.yr_per_d = 1/365 # [yr/d] - cyl.m3_per_cm3 = 1e-6; # m3/cm3 - cyl.cm3_per_m3 = 1e6; # cm3/m3 - cyl.cm3_per_L = 1e3; # cm3/dm3 - - cyl.kgC_per_mol = (1/1000) * cyl.molarMassC - cyl.kgN_per_mol = (1/1000) * cyl.molarMassN - - yr_per_d = 1/365 # [yr/d] - m3_per_cm3 = 1e-6; # m3/cm3 - cm3_per_m3 = 1e6; # cm3/m3 - - molMarssW = 18 # g/mol - rhoW = 1 #g/cm3 - rhoWM = rhoW/molMarssW #[g/cm3]*[g/mol] = mol/cm3 - cyl.rhoWM = rhoWM - mlFr=1/rhoWM - - cyl.molarMassC = 12.011 - cyl.molarMassN = 14.0 - cyl.mg_per_molC = cyl.molarMassC * 1000. - cyl.mg_per_molN = cyl.molarMassN * 1000. - yr_per_d = 1/365 # [yr/d] - m3_per_cm3 = 1e-6; # m3/cm3 - cm3_per_m3 = 1e6; # cm3/m3 - - molMarssW = 18 # g/mol - molMassMulC = 30 #g/mol - rhoW = 1 #g/cm3 - rhoWM = rhoW/molMarssW #[g/cm3]*[g/mol] = mol/cm3 - cyl.mlFr=1/rhoWM #only water phase is relevant right now - - - - - return cyl -def getSoilTextureAndShape(s, soilVG): - """ soil shape and texture data - to adapt according to the soil represented - """ - - constants = {'solidDensity': 2650, # kg/m3 - 'solidMolarMass': 60.08e-3 #kg/mol - } - paramSet = pd.DataFrame() - - solidDensity = 2650 # [kg/m^3 solid] #taken from google docs TraiRhizo - solidMolarMass = 60.08e-3 # [kg/mol] - # theta_r, theta_s, alpha, n, Ks - #soilVG = [paramSet['soilVG_theta_r'],paramSet['soilVG_theta_s'],paramSet['soilVG_alpha'],paramSet['soilVG_n'],paramSet['soilVG_Ks']] - soilTextureAndShape = { - "solidDensity":solidDensity, - 'solidMolarMass': solidMolarMass, - 'soilVG':soilVG} - #common transformations - - soilTexture =soilTextureAndShape - - s.solidDensity = soilTexture['solidDensity'] #[kg/m^3 solid] - s.solidMolarMass = soilTexture['solidMolarMass']# [kg/mol] - s.soil = soilTexture['soilVG'] - - s.vg_soil = vg.Parameters(s.soil) - # [mol / m3 solid] =[kg/m^3 solid] / [kg/mol] - s.solidMolDensity = s.solidDensity/s.solidMolarMass - # [mol / m3 scv] = [mol / m3 solid] * [m3 solid /m3 space] - s.bulkDensity_m3 = s.solidMolDensity*(1.- s.vg_soil.theta_S) - # (kg/m3) * (volume soil / total volume) * (g/kg) * (m3/cm3) - s.bulkMassDensity_gpercm3 = s.solidDensity*(1.- s.vg_soil.theta_S)*1000*1e-6 - - s.masspercm3 = s.solidDensity * 1.e-6 - return soilTexture def run_perirhizal_test(): #space for the outputs water_dumux = np.zeros((n_times, n_sp+1)) + water2_dumux = np.zeros((n_times, n_sp+1)) solute_dumux = np.zeros((n_times, n_sp+1)) water_ss = np.zeros((n_times, n_sp+1)) solute_ss = np.zeros((n_times, n_sp+1)) water_sr = np.zeros((n_times, n_sp+1)) + water_test = np.zeros((n_times, n_sp+1)) solute_sr = np.zeros((n_times, n_sp+1)) + mp_perirhizal2 = np.zeros((n_times, n_sp+1)) + mean_water = np.zeros((n_times)) mean_solutes = np.zeros((n_times)) # determine some random parameters - initial_waterpotential = -3000 + np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? + initial_waterpotential = -100 #+ np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? initial_soluteconcentration = 2e-5*(1.0+np.random.rand()) #g/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? print("initial_soluteconcentration",initial_soluteconcentration) # root conductivity and solute uptake parameters, it is chosen to be constant throughout the entire simulation time root_conductivity = 1e-4 #1/d inner_kr = root_conductivity * r_root * 2 * 3.14 - Vmax = 4.0e-11 * 62 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * (g/mol) * cm * cm * (s/d) -> g / d #TODO: remove test multiplication by 10 + waterdemand = -0.1 #cm/d + Vmax = 4.0e-11 * 62 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * (g/mol) * cm * cm * (s/d) -> g / d Km = 1.5e-7 * 62 #mol/cm3 -> g/cm3 #DS_W = 1.902e-5 #cm2/s #Ds = DS_W / 10000#m2/s Ds = 1.902e-5 #cm2/s - Ds = Ds * 24 * 3600 #cm2/d #TODO: remove the division by 3 after testing + Ds = Ds * 24 * 3600 #cm2/d # the xylem matrix potential varies over time (keep it low so that there is little to no outflow of water) - rx_t = lambda t : -7000+200*np.sin(t) #cm + rx_t = lambda t : -1000+200*np.sin(t) #cm + rx_t = lambda t : -14000+0*np.sin(t) #cm + rx_t = lambda t : -1000-200*t #cm # load the perirhizal model peri = PerirhizalPython() @@ -184,14 +120,15 @@ def run_perirhizal_test(): s = RichardsWrapper(RichardsNCCylFoam()) s.initialize() - s.createGrid1d(np.linspace(r_root, r_prhiz, NC), length = length/100) # [cm] + s.createGrid1d(np.linspace(r_root, r_prhiz, NC), length = length/100) # [m] -> [cm] s.setVGParameters([soilVG]) # theta = 0.378, benchmark is set be nearly fully saturated, so we don't care too much about the specific values s.setHomogeneousIC(initial_waterpotential) # cm pressure head s.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")# - s.setBotBC("constantFluxCyl",0.0) # "noFlux")# + #s.setBotBC("constantFluxCyl",0.0) # "noFlux")# + s.setBotBC("constantFluxCyl",waterdemand) # "noFlux")# Flux in cm/d s.setParameter("Soil.BC.Top.SType", "3") # michaelisMenten=8 (SType = Solute Type) s.setParameter("Soil.BC.Top.CValue", "0.0") # michaelisMenten=8 (SType = Solute Type) s.setParameter("Soil.BC.Bot.SType", "3") # michaelisMenten=8 (SType = Solute Type) @@ -229,6 +166,8 @@ def run_perirhizal_test(): #model root water uptake rx = rx_t(r*dt) + + current_rs_potential = s.getSolutionHead() water_dumux[r,1:] = vg.water_content(current_rs_potential,peri.sp) current_rs_potential = current_rs_potential[0] @@ -237,6 +176,8 @@ def run_perirhizal_test(): print(water_dumux) #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) + #alternative: adapt root matrix potential for a steady uptake: + rx = max(waterdemand / root_conductivity + current_rs_potential, -14750) #note: waterdemand is assumed to be negative #model root solute uptake #note: solutes are given in g @@ -249,7 +190,7 @@ def run_perirhizal_test(): #s.setParameter( "Soil.BC.Bot.SValue", str(root_soluteuptake)) #s.initializeProblem(maxDt = 0.01) - s.setSource({0: root_wateruptake * length}) + #s.setSource({0: root_wateruptake * length}) s.setSource({0: root_soluteuptake * length}, eq_idx = 1) s.solve(dt, saveInnerFluxes_ = True) @@ -262,59 +203,6 @@ def run_perirhizal_test(): soilSoilFluxes = s.getOuterFlow(0, length) * dt # cm3 soilSoilFluxesS = s.getOuterFlow(1, length) * dt # g - # TODO: currently, setSource not properly implemented for richards and richards 2c - # so left out of the mass balance. - # scvSources = s.getSource(0) * cellVolumes * dt # cm3 - # scvSourcesS = s.getSource(1) * cellVolumes * dt # kg - - # #cyl.initializeProblem(maxDt=cyl.maxDt ) - # #boundary conditions - # #water - # cyl.setParameter( "Soil.BC.Bot.Type", str(int(3))) - # cyl.setParameter( "Soil.BC.Top.Type", str(int(3))) - # cyl.setParameter( "Soil.BC.Bot.Value", str(0.0)) #will be prescribed at each timestep - # cyl.setParameter( "Soil.BC.Top.Value", str(0.0)) - - # #solutes - # s.setParameter( "Soil.BC.Bot.SType", str(int(3))) # TODO change this to 8 as Michaelis mnetne - # s.setParameter( "Soil.BC.Top.SType", str(int(3))) - # s.setParameter( "Soil.BC.Bot.CValue", str(0.0)) #Michaelis Menten parameters are set elsewhere - # s.setParameter( "Soil.BC.Top.CValue", str(0.0)) - # print("test") - - # # run the dumux model - # #get water and solute next to the root - # current_rs_potential = s.getSolutionHead()[0] #todo rmeove - # print(current_rs_potential) - # current_rs_concentration = s.getSolution(1)[0] * s.rhoWM # todo remove - # print(current_rs_concentration[0]) - # for i in range(n_times): - # rx = rx_t(i*dt) - # #model root water uptake - # root_wateruptake = root_conductivity * (rx - current_rs_potential) - # water_dumux[i,0] = root_wateruptake - # #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) - # #s.setSource( 0, str(root_wateruptake)) #TODO activat ehtis - # print("test") - # #model root solute uptake - # root_soluteuptake = Vmax * current_rs_concentration * (Km + current_rs_concentration) - # solute_dumux[i,0] = root_wateruptake - # #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) #todo set BC solute - # print("test5") - # s.solve(dt) - - # # save outputs of dumux - # print("test6") - # water_dumux[i,1:] = cyl.getWaterContent() - # solute_dumux[i,1:] = cyl.getSolution(0) - - # mean_water = np.mean(water_dumux[i,1:]) - # mean_solutes = np.mean(solutes_dumux[i,1:]) - - - # current_rs_potential = cyl.getSolutionHead() - # current_rs_potential = current_rs_potential[0] - # current_rs_concentration = solute_dumux[i,1] @@ -342,13 +230,39 @@ def run_perirhizal_test(): #compute the spatial watercontents Phi_A, Phi_C = peri.determine_mfp_function(Phi_root, Phi_soil, rho) + + #compute Phi_A based on the water demand + #Phi_A = waterdemand * r_root / (2*(1-rho**2)) + #Phi_C = + #compute Phi_C based on the result + #outer Phi Phi_out = Phi_A+Phi_C + print("Phiroot", Phi_root, "Phi_soil", Phi_soil, "Phi_A", Phi_A, "Phi_C", Phi_C) + r_rel = CC[0] / r_prhiz + print("Phi_out", Phi_out, "Phi_in", Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C) for j in range(NC-1): r_rel = CC[j] / r_prhiz Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C - water_sr[r,j+1] = vg.water_content(vg.fast_imfp[sp](Phi),peri.sp) + #Phi_soil = Phi_out # TODO: remove this + #if water_dumux[r,j+1] Date: Wed, 27 May 2026 10:50:35 +0200 Subject: [PATCH 16/41] benchmark waterstress works --- src/functional/Perirhizal.py | 44 ++++++++++++++++++++++ test/test_perirhizal.py | 73 +++++++++++++++++++++++++++--------- 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index ecde6f757..f58a3969a 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -200,6 +200,50 @@ def soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m): rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) return rsx.root + def solutesuptake_RooseKirk_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): + """ + finds solute concentration at the soil root interface for all segments following T. Roose and G. Kirk 2009 doi:10.1007/s11104-008-9777-z + + It assumes a constant watercontent with a constant waterflow + + Phi_root matrix flux potential at the root-soil-interface [cm2/d] + Phi_soil matrix flux potential at the bulk soil [cm2/d] + c_bulk solute concentration at the bulk soil [mol/cm3] + Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] + Km half saturation constant Michaelis Menten Kinetics [mol/cm3] + Ds Diffusion constant in water [cm2/d] + waterflow steady state waterflow (entire root circumference) [cm2/d] + sp van Genuchten parameter set + """ + assert len(Phi_root) == len(Phi_soil) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" + + n_segments = len(c_bulk) + + rsc = np.zeros(n_segments) + F = np.zeros(n_segments) #F is a helper values + F_tilde_inv = np.zeros(n_segments) + + if self.lookup_table_solutes: + F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, len(c_bulk))] + else: + F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] + + #compute a prefactor + D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) + + #solve quadratic eqation # TODO: Link to publication + for i in range(n_segments): + print("Dtilde",D_tilde,"F",F[i]) + F_tilde_inv[i]=math.exp(-D_tilde*F[i]) + a1=c_bulk[i]*F_tilde_inv[i] + a2=(1-F_tilde_inv[i])/(waterflow[i]) + p=Km[i]-a2*Vmax[i]-a1 + q=-Km[i]*a1 + rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) + + + return rsc + def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): """ finds solute concentration at the soil root interface for all segments assuming steady state diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 378db0061..c5ecc1903 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -24,15 +24,17 @@ import matplotlib.pyplot as plt from plantbox.visualisation import figure_style +from scipy.optimize import fsolve, root_scalar + # run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model n_tests = 1 #try everything here for this many random parameter sets -do_computation = True #should the computation be run or take the data from a saved file +do_computation = False #should the computation be run or take the data from a saved file # general parameters -max_time = 40 #d -n_times = 400 # number of time intervals +max_time = 25 #d +n_times = 500 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 0.6 # cm r_root = 0.02 # cm @@ -54,6 +56,7 @@ watercontent_steadyrate2 = np.zeros((n_tests, n_times, n_sp+1)) soluteconcentration_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) +matrixpotential_perirhizal1 = np.zeros((n_tests, n_times, n_sp+1)) matrixpotential_perirhizal2 = np.zeros((n_tests, n_times, n_sp+1)) soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil @@ -71,7 +74,7 @@ def run_perirhizal_test(): #space for the outputs water_dumux = np.zeros((n_times, n_sp+1)) - water2_dumux = np.zeros((n_times, n_sp+1)) + water_sr2 = np.zeros((n_times, n_sp+1)) solute_dumux = np.zeros((n_times, n_sp+1)) water_ss = np.zeros((n_times, n_sp+1)) solute_ss = np.zeros((n_times, n_sp+1)) @@ -79,6 +82,7 @@ def run_perirhizal_test(): water_test = np.zeros((n_times, n_sp+1)) solute_sr = np.zeros((n_times, n_sp+1)) + mp_perirhizal1 = np.zeros((n_times, n_sp+1)) mp_perirhizal2 = np.zeros((n_times, n_sp+1)) mean_water = np.zeros((n_times)) @@ -242,9 +246,21 @@ def run_perirhizal_test(): r_rel = CC[0] / r_prhiz print("Phi_out", Phi_out, "Phi_in", Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C) + #determine the critical matrix potential + h_out = [-200, -800, -3200, -6000,-10000,-13700, -14000] # cm + r_rel = 1/rho + MFP_root = lambda Phi : Phi+abs(waterdemand)*r_root*((r_rel*rho)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + MFP_root_stress = lambda Phi : Phi*((r_rel*rho)**2 - 1 + 2*rho**2*np.log(1/(r_rel*rho))/(rho**2 - 1 + 2*rho**2*np.log(1/rho))) + print(MFP_root_stress(vg.fast_mfp[sp](h_out[0])),MFP_root_stress(vg.fast_mfp[sp](h_out[-1]))) + MFP_out = root_scalar(MFP_root, method="brentq", bracket=[ vg.fast_mfp[sp](h_out[0]), vg.fast_mfp[sp](h_out[-1])]).root + print(MFP_out) + #MFP_out = MFP_out /10000 + + for j in range(NC-1): r_rel = CC[j] / r_prhiz Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C + mp_perirhizal1[r,j+1]=vg.fast_imfp[sp](Phi) #Phi_soil = Phi_out # TODO: remove this #if water_dumux[r,j+1] Date: Tue, 2 Jun 2026 16:32:28 +0200 Subject: [PATCH 17/41] steady rate solute works --- src/functional/Perirhizal.py | 141 +++++++++++++++++++++++++------ test/test_perirhizal.py | 158 ++++++++++++++++++++++++----------- 2 files changed, 224 insertions(+), 75 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index f58a3969a..96c06aa2e 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -137,7 +137,7 @@ def soil_root_interface_(rx, sx, inner_kr, rho, sp): #print(vg.fast_mfp[sp](-16000)) if inner_kr < 1.0e-7: return sx - k_soilfun = lambda hsoil, hint: (vg.fast_mfp[sp](hsoil) - vg.fast_mfp[sp](hint)) / (hsoil - hint + 0.001) # Vanderborgth et al. 2023, Eqn [7] + k_soilfun = lambda hsoil, hint: (vg.fast_mfp[sp](hsoil) - vg.fast_mfp[sp](hint)) * (hsoil - hint) / ((hsoil - hint)**2 + 0.001) # Vanderborgth et al. 2023, Eqn [7] rho2 = np.square(rho) # rho squared b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] fun = lambda x: (inner_kr * rx + b * sx * k_soilfun(sx, x)) / (b * k_soilfun(sx, x) + inner_kr) - x @@ -200,51 +200,134 @@ def soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m): rsx = root_scalar(fun, method="brentq", bracket=[x_int[0], x_int[1]]) return rsx.root - def solutesuptake_RooseKirk_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): + def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, r_root, E, t, sp): """ finds solute concentration at the soil root interface for all segments following T. Roose and G. Kirk 2009 doi:10.1007/s11104-008-9777-z - It assumes a constant watercontent with a constant waterflow - - Phi_root matrix flux potential at the root-soil-interface [cm2/d] - Phi_soil matrix flux potential at the bulk soil [cm2/d] + It assumes a constant watercontent with a constant wateruptake by the root + + watercontent watercontent of the perirhizal zone [cm3/cm3] c_bulk solute concentration at the bulk soil [mol/cm3] Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] waterflow steady state waterflow (entire root circumference) [cm2/d] + r_root root radius [cm] + E minimum net influx into the plant [mol/cm2d] + t root age [d] sp van Genuchten parameter set """ - assert len(Phi_root) == len(Phi_soil) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" + assert len(watercontent) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow) == len(r_root) == len(E) == len(t), "error in Perirhizal.py, solutesuptake_convdiff_: watercontent, c_bulk, Vmax, Km, Ds, waterflow, r_root, E and t must have the same length" n_segments = len(c_bulk) + segLength = 1 #reference segment length, should not impact the results [cm] - rsc = np.zeros(n_segments) - F = np.zeros(n_segments) #F is a helper values - F_tilde_inv = np.zeros(n_segments) + rsc = np.zeros(n_segments) # solute concentration at the root soil interface + F = np.zeros(n_segments) # uptake of solutes mol/(cm2d) + gamma = 0.577 # Eulers constant + l_func = lambda time : 1/2*np.log(4*np.exp(-gamma)*time+1) - if self.lookup_table_solutes: - F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, len(c_bulk))] - else: - F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] - #compute a prefactor - D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) + for i in range(n_segments): + #compute diffusion coefficient according to Millington and Quirk + D = Ds * math.pow(watercontent[i], 10/3) / (sp.theta_S**2) + + #unit conversions at the end + unitconversion_F = Km[i] * D * r_root[i] # to [mol/cm2d] #TODO: look at this again, it doesn't seem right + unitconversion_c = Km[i] # to [mol/cm3] + + #express the inputs without units + c_inf = c_bulk[i] / Km[i] + Pe = waterflow[i]/D #Peclet number + lamb = segLength*r_root[i]*Vmax[i]/(D*Km[i]) + epsilon = segLength*r_root[i]*E[i]/(D*Km[i]) + + #compute the unitless uptake + if Pe<=(lamb/(1+c_inf)): + F[i] = 2*lamb*(c_inf+epsilon*l_func(t[i])) / (1+c_inf+l_func(t[i])*(lamb + epsilon) +math.sqrt(4*(c_inf+epsilon * l_func(t[i]))+(1-c_inf+(lamb-epsilon)*l_func(t[i]))**2)) #Eqn. [9] + else: + F[i] = 2*lamb*(c_inf+epsilon*l_func(t[i])) / (1+c_inf+l_func(t[i])*(lamb - Pe + epsilon) +math.sqrt(4*(c_inf+epsilon * l_func(t[i]))*(1-Pe*l_func(t[i]))+(1-c_inf+l_func(t[i])*(lamb-Pe-epsilon))**2)) #Eqm. [10] + #compute the unitless solute concentration next to the root + #F(t) = lamb * c / (1+c) - epsilon shortly after eq. (10) + #(1+c)*(F(t)+epsilon) = lamb * c + #F(t)+epsilon = (-F(t)-epsilon+lamb) * c + rsc[i] = (F[i]+epsilon)/(lamb-F[i]-epsilon) + + #add units + F[i] = F[i] * unitconversion_F + rsc[i] = rsc[i] * unitconversion_c + + return rsc + + def soil_root_solutes_ss_(self, Phi_soil, rootwateruptake, soilwateruptake, rho, c_bulk, Vmax, Km, Ds, waterflow, sp, output_disc = False): + """ + steady state assumption of solute uptake by roots TODO: insert citaiton + + Phi_out outer matrix flux potential [cm2/d] + rootwateruptake radial root water uptake [cm2/d] + soilwateruptake radial water inflow [cm2/d] + rho outer radius / root radius [cm/cm] + c_soil mean solute concentration in the cylinder [mol/cm3] + Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] + Km half saturation constant Michaelis Menten Kinetics [mol/cm3] + Ds Diffusion constant in water [cm2/d] + sp van Genuchten parameter set + output_disc should the discretisation be put out + + output: + c_root solute concnetration next to the root [mol/cm3] + Uptake solute uptake of the root [mol/(cm d)] + disc discretisation of the solute computation [mol/cm3] + + """ + assert len(Phi_A) == len(Phi_C) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" + + n_segments = len(c_bulk) + + c_root = np.zeros(n_segments) + Uptake = np.zeros(n_segments) + + + #equations #TODO: remove them here + #U = d_r(c) * Ds * r + c * (waterflow * r) = const + #1 / (Ds * r) = d_r(c/U) + (c/U) * (waterflow * r) / (Ds * r) + #(c/U)(r) = exp(-int((waterflow * r) / (Ds * r))) * z(r) * (c/U)(start) + # z'(r) = exp(int((waterflow * r) / (Ds * r))) / (Ds * r), z(start) = 1 #solve quadratic eqation # TODO: Link to publication for i in range(n_segments): - print("Dtilde",D_tilde,"F",F[i]) - F_tilde_inv[i]=math.exp(-D_tilde*F[i]) - a1=c_bulk[i]*F_tilde_inv[i] - a2=(1-F_tilde_inv[i])/(waterflow[i]) - p=Km[i]-a2*Vmax[i]-a1 - q=-Km[i]*a1 - rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) + Phi = lambda r_rel : Phi_out + (rootwateruptake-soilwateruptake)*(r_rel**2*rho**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(rho/r_rel)-0.5)) + soilwateruptake*np.log(r_rel) + radial_waterflow = lambda r_rel : soilwateruptake + (rootwateruptake-soilwateruptake) * (1 - r_rel**2) / (1-1/rho**2) + Ds = lambda r_rel : Ds * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel), self.sp)),10/3) / (sp.theta_S**2) # Millington and Quirk + + return Vtilde, Ktilde + + def soil_root_solutes_combined(self, Phi_soil, rootwateruptake, soilwateruptake, c_bulk, Vmax, Km, Ds, waterflow, sp, output_disc = False): + """ + combines a steady state assumption of solutes next to the roots with the far field approximation by Tiina Roose TODO: insert citaitons + The cutoff happens where the diffusion coefficient (following the Millington Quirk model) reaches 95% that of the mean diffusion coefficient + + Phi_out outer matrix flux potential [cm2/d] + rootwateruptake radial root water uptake [cm2/d] + soilwateruptake radial water inflow [cm2/d] + c_soil mean solute concentration in the cylinder [mol/cm3] + Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] + Km half saturation constant Michaelis Menten Kinetics [mol/cm3] + Ds Diffusion constant in water [cm2/d] + waterflow steady state waterflow (entire root circumference) [cm2/d] + sp van Genuchten parameter set + output_disc should the discretisation be put out + + output: + c_root solute concnetration next to the root [mol/cm3] + disc discretisation of the solute computation [mol/cm3] - return rsc + """ + - def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): + + def soil_root_solutes_ss_old(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): """ finds solute concentration at the soil root interface for all segments assuming steady state uses a look up tables if present (see create_lookup, and open_lookup) @@ -275,6 +358,7 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) #solve quadratic eqation # TODO: Link to publication + print("test",Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow) #TODO remove for i in range(n_segments): print("Dtilde",D_tilde,"F",F[i]) F_tilde_inv[i]=math.exp(-D_tilde*F[i]) @@ -287,6 +371,7 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf return rsc + def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, waterflow, sp, approximate_rho = False): """ finds solute concentration at the soil root interface for all segments assuming steady rate @@ -370,11 +455,13 @@ def integral_overDiffusion_(Phi, sp): return integral_overD - def integral_overconcentration_(self, Ds, Phi_root, Phi_soil, rho, sp): + # deprecate this function. It is way to much work and the steady rate solution isn't even close to accurate + # instead the outer concentration can be used + def integral_overconcentration_(self, Ds, Phi_root, Phi_soil, rho, sp): #TODO: use Phi_A and Phi_C as inputs here so that rho can more easily be varied """ (2 pi * 0.9999 rprhiz^2 (c_rs-C_SW))/(Integral from 0.01*rprhiz to rprhiz of 2 pi s (c(s)-C_SW) ds) - Ds Diffusion coefficient + Ds Diffusion coefficient [cm2/d] Phi_root, Phi_soil matrix flux potential at the root and soil [cm2/d] rho r_prhiz / r_root diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index c5ecc1903..64d23c2e5 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -29,12 +29,12 @@ # run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model n_tests = 1 #try everything here for this many random parameter sets -do_computation = False #should the computation be run or take the data from a saved file +do_computation = True #should the computation be run or take the data from a saved file # general parameters -max_time = 25 #d -n_times = 500 # number of time intervals +max_time = 1 #d +n_times = 50 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 0.6 # cm r_root = 0.02 # cm @@ -59,6 +59,8 @@ matrixpotential_perirhizal1 = np.zeros((n_tests, n_times, n_sp+1)) matrixpotential_perirhizal2 = np.zeros((n_tests, n_times, n_sp+1)) +soluteconcentration_Tiina = np.zeros((n_tests, n_times, n_sp+1)) + soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil soilVG = [0.08, 0.43, 0.04, 1.6, 50] #loam from benchmark #da war am Ende noch eine 0.5, keine Ahnung warum, vermutlich eine vorherige Programmversion @@ -68,7 +70,10 @@ NC, base = lb) CC = np.array([(points[i] + points[i+1])/2 for i in range(len(points)-1)]) - +volumes = np.array([(points[i+1]**2 - points[i]**2)*3.14 for i in range(len(points)-1)]) +initial_waterpotential = -300 +initial_soluteconcentration = 2e-5#mol/cm3 +outer_conc = initial_soluteconcentration def run_perirhizal_test(): @@ -88,22 +93,31 @@ def run_perirhizal_test(): mean_water = np.zeros((n_times)) mean_solutes = np.zeros((n_times)) + solutes_Tiina = np.zeros((n_times, n_sp+1)) + # determine some random parameters - initial_waterpotential = -100 #+ np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? - initial_soluteconcentration = 2e-5*(1.0+np.random.rand()) #g/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? + #initial_waterpotential = -700 #+ np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? + #initial_soluteconcentration = 2e-5#*(1.0+np.random.rand()) #g/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? print("initial_soluteconcentration",initial_soluteconcentration) # root conductivity and solute uptake parameters, it is chosen to be constant throughout the entire simulation time root_conductivity = 1e-4 #1/d inner_kr = root_conductivity * r_root * 2 * 3.14 waterdemand = -0.1 #cm/d - Vmax = 4.0e-11 * 62 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * (g/mol) * cm * cm * (s/d) -> g / d - Km = 1.5e-7 * 62 #mol/cm3 -> g/cm3 + radial_waterdemand = 2*3.14*r_root * waterdemand #cm2/d + Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d + Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) + Km = 1.5e-7 #mol/cm3 #DS_W = 1.902e-5 #cm2/s #Ds = DS_W / 10000#m2/s - Ds = 1.902e-5 #cm2/s + Ds = 1.902e-5 / 2#cm2/s # division by 2: from NO3 to H2PO4 Ds = Ds * 24 * 3600 #cm2/d + + outer_waterpotential = initial_waterpotential #cm + + outer_kr = root_conductivity * r_prhiz * 2 * 3.14 #TODO: this is just for testing purposes as I do not want a Dirichlet BC as it would be mixed BC + outer_conc = initial_soluteconcentration # the xylem matrix potential varies over time (keep it low so that there is little to no outflow of water) rx_t = lambda t : -1000+200*np.sin(t) #cm @@ -116,6 +130,7 @@ def run_perirhizal_test(): peri.set_soil(sp) #no lookup tables are used here as there arent many simulations + outer_watercontent = vg.water_content(outer_waterpotential, peri.sp) simtimes = np.linspace(max_time/n_times,max_time,n_times).tolist() # days #TODO remove @@ -131,6 +146,7 @@ def run_perirhizal_test(): s.setHomogeneousIC(initial_waterpotential) # cm pressure head s.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")# + #s.setTopBC("constantPressure",-200) # [cm/day] "noFlux")# #s.setBotBC("constantFluxCyl",0.0) # "noFlux")# s.setBotBC("constantFluxCyl",waterdemand) # "noFlux")# Flux in cm/d s.setParameter("Soil.BC.Top.SType", "3") # michaelisMenten=8 (SType = Solute Type) @@ -157,6 +173,7 @@ def run_perirhizal_test(): for r, dt in enumerate(dt_): + dampening = 0.01 #slow down the inflow from outside time = simtimes[r] print('time',time) @@ -172,40 +189,47 @@ def run_perirhizal_test(): rx = rx_t(r*dt) - current_rs_potential = s.getSolutionHead() - water_dumux[r,1:] = vg.water_content(current_rs_potential,peri.sp) - current_rs_potential = current_rs_potential[0] + current_potential = s.getSolutionHead() + water_dumux[r,1:] = vg.water_content(current_potential,peri.sp) + current_rs_potential = current_potential[0] + current_outer_potential = current_potential[-1] + current_outer_watercontent = vg.water_content(current_outer_potential, peri.sp) root_wateruptake = inner_kr * (rx - current_rs_potential) + outer_watersource = outer_kr * (outer_waterpotential - current_outer_potential) + outer_watersource = (outer_watercontent - current_outer_watercontent) /dt * dampening water_dumux[r,0] = root_wateruptake print(water_dumux) #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) #alternative: adapt root matrix potential for a steady uptake: rx = max(waterdemand / root_conductivity + current_rs_potential, -14750) #note: waterdemand is assumed to be negative + #rx = current_rs_potential #model root solute uptake #note: solutes are given in g - current_rs_concentration = s.getSolution(1) - solute_dumux[r,1:] = current_rs_concentration - current_rs_concentration = current_rs_concentration[0] - root_soluteuptake = - Vmax * current_rs_concentration / (Km + current_rs_concentration) + current_concentration = s.getSolution(1) + solute_dumux[r,1:] = current_concentration + current_rs_concentration = current_concentration[0] + current_outer_concentration = current_concentration[-1] + root_soluteuptake = - Vmax * max(current_rs_concentration,0) / (Km + current_rs_concentration) # mol / d + outer_solutesource = ( outer_conc - current_outer_concentration ) * current_outer_watercontent / dt * dampening solute_dumux[r,0] = root_soluteuptake print(solute_dumux) #s.setParameter( "Soil.BC.Bot.SValue", str(root_soluteuptake)) #s.initializeProblem(maxDt = 0.01) - #s.setSource({0: root_wateruptake * length}) - s.setSource({0: root_soluteuptake * length}, eq_idx = 1) + s.setSource({NC-2: 1 * outer_watersource * volumes[-1] *length}) + s.setSource({0: root_soluteuptake * 62, NC-2: 1* outer_solutesource * volumes[-1] * length * 62}, eq_idx = 1) #factor 1000 is important for dumux as in the richards wrapper it is divided by 1000 s.solve(dt, saveInnerFluxes_ = True) Wvolafter = cellVolumes*s.getWaterContent() # cm3 - Smassafter = s.getSolution(1) * Wvolafter # g + Smassafter = s.getSolution(1) * Wvolafter # mol rootSoilFluxes = s.getInnerFlow(0, length) * dt # cm3 - rootSoilFluxesS = s.getInnerFlow(1, length) * dt # g + rootSoilFluxesS = s.getInnerFlow(1, length) * dt # mol soilSoilFluxes = s.getOuterFlow(0, length) * dt # cm3 - soilSoilFluxesS = s.getOuterFlow(1, length) * dt # g + soilSoilFluxesS = s.getOuterFlow(1, length) * dt # mol @@ -235,6 +259,17 @@ def run_perirhizal_test(): #compute the spatial watercontents Phi_A, Phi_C = peri.determine_mfp_function(Phi_root, Phi_soil, rho) + #alternative computation using the waterdemand + Phi_A = abs(waterdemand)*r_root*((rho)**2/(2*(1-rho**2))) + Phi_C = Phi_soil - Phi_A + + #if waterstress, then Phi_A = - rho**2*Phi_C + if Phi_A*(1/rho**2-np.log(1/rho**2))+Phi_C <=0: + Phi_A = Phi_soil*rho**2/(rho**2-1) + Phi_C = -Phi_A*(1/rho**2-np.log(1/rho**2)) + + Phi_A_orig = Phi_A + Phi_C_orig = Phi_C #compute Phi_A based on the water demand #Phi_A = waterdemand * r_root / (2*(1-rho**2)) #Phi_C = @@ -251,8 +286,12 @@ def run_perirhizal_test(): r_rel = 1/rho MFP_root = lambda Phi : Phi+abs(waterdemand)*r_root*((r_rel*rho)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) MFP_root_stress = lambda Phi : Phi*((r_rel*rho)**2 - 1 + 2*rho**2*np.log(1/(r_rel*rho))/(rho**2 - 1 + 2*rho**2*np.log(1/rho))) + MFP_mean_difference = lambda Phi : Phi+abs(waterdemand)*r_root*((0.53*rho)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) - Phi_soil print(MFP_root_stress(vg.fast_mfp[sp](h_out[0])),MFP_root_stress(vg.fast_mfp[sp](h_out[-1]))) - MFP_out = root_scalar(MFP_root, method="brentq", bracket=[ vg.fast_mfp[sp](h_out[0]), vg.fast_mfp[sp](h_out[-1])]).root + if MFP_mean_difference(vg.fast_mfp[sp](h_out[0]))*MFP_mean_difference(vg.fast_mfp[sp](h_out[-1]))<0: + MFP_out = root_scalar(MFP_mean_difference, method="brentq", bracket=[ vg.fast_mfp[sp](h_out[0]), vg.fast_mfp[sp](h_out[-1])]).root + else: + MFP_out = 1.e-3 print(MFP_out) #MFP_out = MFP_out /10000 @@ -260,7 +299,8 @@ def run_perirhizal_test(): for j in range(NC-1): r_rel = CC[j] / r_prhiz Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C - mp_perirhizal1[r,j+1]=vg.fast_imfp[sp](Phi) + mp_perirhizal1[r,j+1]=vg.fast_imfp[sp](Phi) # TODO: adapt this to stress maybe? + #Phi_soil = Phi_out # TODO: remove this #if water_dumux[r,j+1] Date: Fri, 12 Jun 2026 10:01:37 +0200 Subject: [PATCH 18/41] subfunction general steady rate --- src/functional/Perirhizal.py | 138 +++++++++++++++++++++++++++++------ test/test_perirhizal.py | 10 +-- 2 files changed, 122 insertions(+), 26 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 96c06aa2e..341216ad5 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -4,8 +4,10 @@ from scipy.interpolate import RegularGridInterpolator from scipy.optimize import fsolve, root_scalar from scipy.spatial import ConvexHull, Voronoi +from scipy.integrate import odeint from scipy import integrate + import math import plantbox as pb @@ -207,7 +209,7 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, It assumes a constant watercontent with a constant wateruptake by the root watercontent watercontent of the perirhizal zone [cm3/cm3] - c_bulk solute concentration at the bulk soil [mol/cm3] + c_bulk starting solute concentration at the bulk soil [mol/cm3] Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] @@ -242,13 +244,15 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, lamb = segLength*r_root[i]*Vmax[i]/(D*Km[i]) epsilon = segLength*r_root[i]*E[i]/(D*Km[i]) + print("Tiinatest", t[i], c_inf,epsilon,l_func(t[i]),Pe,lamb,Pe) + #compute the unitless uptake if Pe<=(lamb/(1+c_inf)): F[i] = 2*lamb*(c_inf+epsilon*l_func(t[i])) / (1+c_inf+l_func(t[i])*(lamb + epsilon) +math.sqrt(4*(c_inf+epsilon * l_func(t[i]))+(1-c_inf+(lamb-epsilon)*l_func(t[i]))**2)) #Eqn. [9] else: F[i] = 2*lamb*(c_inf+epsilon*l_func(t[i])) / (1+c_inf+l_func(t[i])*(lamb - Pe + epsilon) +math.sqrt(4*(c_inf+epsilon * l_func(t[i]))*(1-Pe*l_func(t[i]))+(1-c_inf+l_func(t[i])*(lamb-Pe-epsilon))**2)) #Eqm. [10] #compute the unitless solute concentration next to the root - #F(t) = lamb * c / (1+c) - epsilon shortly after eq. (10) + #F(t) = lamb * c / (1+c) - epsilon shortly after Eqn. [10] #(1+c)*(F(t)+epsilon) = lamb * c #F(t)+epsilon = (-F(t)-epsilon+lamb) * c rsc[i] = (F[i]+epsilon)/(lamb-F[i]-epsilon) @@ -259,15 +263,16 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, return rsc - def soil_root_solutes_ss_(self, Phi_soil, rootwateruptake, soilwateruptake, rho, c_bulk, Vmax, Km, Ds, waterflow, sp, output_disc = False): + def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_bulk, Vmax, Km, Ds, waterflow, sp, output_disc = False): """ steady state assumption of solute uptake by roots TODO: insert citaiton Phi_out outer matrix flux potential [cm2/d] rootwateruptake radial root water uptake [cm2/d] - soilwateruptake radial water inflow [cm2/d] - rho outer radius / root radius [cm/cm] - c_soil mean solute concentration in the cylinder [mol/cm3] + waterinflow radial water inflow [cm2/d] + r_root root radius [cm] + r_prhiz outer radius [cm] + c_soil outer solute concentration of the cylinder [mol/cm3] Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] @@ -275,7 +280,7 @@ def soil_root_solutes_ss_(self, Phi_soil, rootwateruptake, soilwateruptake, rho, output_disc should the discretisation be put out output: - c_root solute concnetration next to the root [mol/cm3] + rsc solute concnetration next to the root [mol/cm3] Uptake solute uptake of the root [mol/(cm d)] disc discretisation of the solute computation [mol/cm3] @@ -286,27 +291,118 @@ def soil_root_solutes_ss_(self, Phi_soil, rootwateruptake, soilwateruptake, rho, c_root = np.zeros(n_segments) Uptake = np.zeros(n_segments) + disc = 0 - - #equations #TODO: remove them here - #U = d_r(c) * Ds * r + c * (waterflow * r) = const - #1 / (Ds * r) = d_r(c/U) + (c/U) * (waterflow * r) / (Ds * r) - #(c/U)(r) = exp(-int((waterflow * r) / (Ds * r))) * z(r) * (c/U)(start) - # z'(r) = exp(int((waterflow * r) / (Ds * r))) / (Ds * r), z(start) = 1 + conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c #solve quadratic eqation # TODO: Link to publication for i in range(n_segments): - Phi = lambda r_rel : Phi_out + (rootwateruptake-soilwateruptake)*(r_rel**2*rho**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(rho/r_rel)-0.5)) + soilwateruptake*np.log(r_rel) + Phi = lambda r_rel : Phi_out + (rootwateruptake-soilwateruptake)*(r_rel**2*rho**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + soilwateruptake*np.log(r_rel) radial_waterflow = lambda r_rel : soilwateruptake + (rootwateruptake-soilwateruptake) * (1 - r_rel**2) / (1-1/rho**2) - Ds = lambda r_rel : Ds * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel), self.sp)),10/3) / (sp.theta_S**2) # Millington and Quirk + Ds_func = lambda r_rel : Ds * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel), self.sp)),10/3) / (sp.theta_S**2) # Millington and Quirk + linearODEterm = lambda r_rel : radial_waterflow(r_rel) / (Ds_func(r_rel) * r_rel) + absoluteODEterm = lambda r_rel : 1 / (Ds_func(r_rel) * r_rel) + Y = odeint(f, ic, t, args=([ka1, ka2, ka3], [kd1, kd2, kd3], [b1, b2, b3])) + + + return rsc, Uptake + + def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_bulk, c_root, Ds0, soluteuptake, sp): + """ + given the water and solute uptake data this computes the discretisation of the steady rate solutions + + """ + + #initialize the lambda functions + watercontent = lambda r : 0 + waterpotential = lambda r : 0 + soluteconcentration = lambda r : 0 + + #simple computaitons + rho = r_prhiz / r_root + #reference radius for the solute implementation is either at the perirhizal radius (steady state water uptake) or at the radius without wateruptake + #in both cases it is scaled by Ds0 / self.Ds0 + r_rel0 = r_prhiz * Ds0 / self.Ds0 + + #water is easy + Phi = lambda r : Phi_soil + (rootwateruptake - waterinflow) * ((r_rel/r_root)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(r_prhiz/r)-0.5)) + waterinflow * np.log(r / r_prhiz) + waterpotential = lambda r : vg.fast_imfp(Phi(r),sp) + watercontent = lambda r : vg.watercontent(Phi(r),sp) + + #solutes + #check if there is a lookup table + if self.lookup_table_sr_solutes: + c_solutes = lambda r: self.lookup_table_sr_solutes((Phi_ref, wateruptake_rel, [r])) + else: + abc + + return watercontent, waterpotential, soluteconcentration + + def solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp): + """ + computes a linear equation from the diffusion advection ODE on the perirhizal solute flow #TODO: insert equation number + + r_rel[0] corresponds to the critical radius at which the permanent wilting point is reached (Phi = 0) + r_rel = 1 corresponds to the perirhizal radius + + Ds0 diffusion coefficient of the solute in water [cm2/d] + critical_wateruptake the theoretical radial wateruptake at the radius for which Phi = 0[cm2/d] + wateruptake_rel wateruptake in each volume, both wateruptake and volume are scaled according to r_rel [cm3/(cm3d)] + r_eval (1d array) the equation is solved from r_rel[0] to r_eval [-1]. + + outputs: - return Vtilde, Ktilde + conc_rel_c ratio of c(r_rel[i]) to c(r_rel[0]) for d_r c(1)=0 + conc_mean_c mean solute concentrations for the steady state solute uptake + inflow_rel_c ratio of c(r_rel[i]) to d_r c(1) for c(1)=0 + inflow_mean_c mean solute concentration for c(1)=0 + Uptake_rel_c ratio of c(r_rel[i]) to rel_soluteUptake for c(1)=0, d_r c(1)=0 (steady rate uptake case) + Uptake_mean_c mean solute concentrations relative to rel_soluteUptake + """ + + r_crit = r_eval[0] + + n = len(r_eval) + + conc_rel_c = np.ones(n) + conc_mean_c = np.ones(n) + inflow_rel_c = np.ones(n) + inflow_mean_c = np.ones(n) + Uptake_rel_c = np.ones(n) + Uptake_mean_c = np.ones(n) + + #used for weighting + watercontent_times_r = np.ones(n) + + #waterflow + Phi = lambda r_rel : wateruptake_rel*(r_rel**2/2 - r_crit**2/2 - np.log(r_rel / r_crit)) #[cm2/d] + radial_waterflow = lambda r_rel : critical_wateruptake - wateruptake_rel*(r_rel**2 - r_crit**2)*pi + watercontent = lambda r_rel : vg.water_content(vg.fast_imfp[sp](Phi(r_rel)), sp) + Ds = lambda r_rel : Ds0 * math.pow(watercontent(r_rel),10/3) / (sp.theta_S**2) + + f_homogen = lambda r_rel, c : ( - radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference + f_steadystate = lambda r_rel, c : (1 - radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference + f_steadyrate = lambda r_rel, c : ((r_rel**2 - r_crit**2)*pi - radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference + + #numerically solve the ODE c' = f(r_rel,c) starting at r_rel=1 + conc_rel_c = solve_ivp(f_homogen, t_span = r_eval[-1], 1, t_eval = r_eval) + inflow_rel_c = solve_ivp(f_steadystate, t_span = r_eval[-1], 0, t_eval = r_eval) + Uptake_rel_c = solve_ivp(f_steadyrate, t_span = r_eval[-1], 0, t_eval = r_eval) + + #compute the means + watercontent_times_r = np.array([watercontent(r_rel)*r_rel for r_rel in r_eval]) + conc_mean_c = np.array([np.average(conc_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(n)]) + inflow_mean_c = np.array([np.average(inflow_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(n)]) + Uptake_mean_c = np.array([np.average(Uptake_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(n)]) + + return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c - def soil_root_solutes_combined(self, Phi_soil, rootwateruptake, soilwateruptake, c_bulk, Vmax, Km, Ds, waterflow, sp, output_disc = False): + def soil_root_solutes_combined(self, Phi_soil, rootwateruptake, soilwateruptake, c_prhiz, c_bulk, Vmax, Km, Ds, waterflow, sp): """ combines a steady state assumption of solutes next to the roots with the far field approximation by Tiina Roose TODO: insert citaitons - The cutoff happens where the diffusion coefficient (following the Millington Quirk model) reaches 95% that of the mean diffusion coefficient + In the perirhizal zone it is a steady rate uptake with defined mean concentration. + The relation of c(r_prhiz) and d_r c(r_prhiz) is given by Roose Phi_out outer matrix flux potential [cm2/d] rootwateruptake radial root water uptake [cm2/d] @@ -324,10 +420,10 @@ def soil_root_solutes_combined(self, Phi_soil, rootwateruptake, soilwateruptake, disc discretisation of the solute computation [mol/cm3] """ + return 1 - - def soil_root_solutes_ss_old(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): + def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): """ finds solute concentration at the soil root interface for all segments assuming steady state uses a look up tables if present (see create_lookup, and open_lookup) @@ -533,7 +629,7 @@ def perirhizal_conductance_per_layer(self, h_bs, h_sr, sp): l_root = rld * dz # [cm-1] return 2 * np.pi * np.multiply(l_root, np.multiply(b, k_prhiz(h_bs, h_sr))) # [day-1], see Vanderborght et al. (2023), Eqn [6] - # depricated, leave the function in in case we ever want to compute big lookup tables again + # depricated, leave the function in, in case we ever want to compute big lookup tables again def create_lookup_mpi(self, filename, sp): """ depricated diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 64d23c2e5..802d80732 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -33,8 +33,8 @@ # general parameters -max_time = 1 #d -n_times = 50 # number of time intervals +max_time = 10 #d +n_times = 500 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 0.6 # cm r_root = 0.02 # cm @@ -375,7 +375,7 @@ def run_perirhizal_test(): solute_ss[r,j+1] = result_solutes_ss * F_tilde - (1-F_tilde) * solute_ss[r,0] / abs(waterdemand)#waterdemand is assumed to be negative solute_sr[r,j+1] = result_solutes_sr * F_tilde - (1-F_tilde) * solute_sr[r,0] / abs(waterdemand)#waterdemand is assumed to be negative - solutes_Tiina[r,0] = peri.solutesuptake_convdiff_([mean_water],[outer_conc], [Vmax], [Km], Ds, [waterflow], [r_root], [0.], [time], peri.sp)[0] + solutes_Tiina[r,0] = peri.solutesuptake_convdiff_([mean_water],[outer_conc], [Vmax], [Km], Ds, [waterflow], [r_root], [0.], [time/1.5], peri.sp)[0] return water_dumux, solute_dumux, solute_ss, water_sr, solute_sr, water_sr2, mp_perirhizal1, mp_perirhizal2, solutes_Tiina if do_computation: @@ -501,8 +501,8 @@ def run_perirhizal_test(): #ax1[i].plot(CC, mp_steadyrate2, "b", linestyle = linestyle_steadystate, label = "water_sr_stress") ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") #ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") - #ax2.plot(CC, solute_steadyrate * solute_dumux[-1] / solute_steadyrate[-1], "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - ax2.plot(CC, solute_steadystate * solute_dumux[-1] / solute_steadystate[-1], "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") + ax2.plot(CC, solute_steadyrate * solute_dumux[-1] / solute_steadyrate[-1], "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + ax2.plot(CC, solute_steadystate * solute_dumux[10] / solute_steadystate[10], "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") #ax2.plot(CC, solute_Tiina, "m", linestyle = linestyle_steadystate, label = "solute_Tiina") ax2.plot(CC, solute_Tiina * solute_dumux[-1] / outer_conc, "m", linestyle = linestyle_special, label = "solute_Tiina_scaled") From fce5b88cd89fbe4abd68c1a3fa597a15d6726b10 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sat, 13 Jun 2026 21:53:55 +0200 Subject: [PATCH 19/41] general sr solute uptake implementation --- src/functional/Perirhizal.py | 168 ++++++++++++++++++++++++++++++----- 1 file changed, 144 insertions(+), 24 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 341216ad5..13ef9bc1a 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -263,52 +263,164 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, return rsc - def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_bulk, Vmax, Km, Ds, waterflow, sp, output_disc = False): + def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_bulk, Vmax, Km, Ds, sp, mode = "sr_ff"): """ steady state assumption of solute uptake by roots TODO: insert citaiton - Phi_out outer matrix flux potential [cm2/d] + Phi_soil outer matrix flux potential [cm2/d] rootwateruptake radial root water uptake [cm2/d] - waterinflow radial water inflow [cm2/d] + waterinflow radial water inflow at r_prhiz [cm2/d] r_root root radius [cm] r_prhiz outer radius [cm] - c_soil outer solute concentration of the cylinder [mol/cm3] + c_soil solute concentration of the cylinder [mol/cm3] + c_outer solute concentration outside of the cylinder [mol/cm3], only used in the general steady rate case Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] sp van Genuchten parameter set - output_disc should the discretisation be put out + mode what model is used? pure ss, sr with no flux or sr with the far field approximation output: - rsc solute concnetration next to the root [mol/cm3] + rsc solute concentration next to the root [mol/cm3] Uptake solute uptake of the root [mol/(cm d)] - disc discretisation of the solute computation [mol/cm3] """ assert len(Phi_A) == len(Phi_C) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" n_segments = len(c_bulk) - c_root = np.zeros(n_segments) + rsc = np.zeros(n_segments) Uptake = np.zeros(n_segments) - disc = 0 - - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c + ss_uptake = np.zeros(n_segments) + sr_uptake = np.zeros(n_segments) - #solve quadratic eqation # TODO: Link to publication + #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): - Phi = lambda r_rel : Phi_out + (rootwateruptake-soilwateruptake)*(r_rel**2*rho**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + soilwateruptake*np.log(r_rel) - radial_waterflow = lambda r_rel : soilwateruptake + (rootwateruptake-soilwateruptake) * (1 - r_rel**2) / (1-1/rho**2) - Ds_func = lambda r_rel : Ds * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel), self.sp)),10/3) / (sp.theta_S**2) # Millington and Quirk - linearODEterm = lambda r_rel : radial_waterflow(r_rel) / (Ds_func(r_rel) * r_rel) - absoluteODEterm = lambda r_rel : 1 / (Ds_func(r_rel) * r_rel) - Y = odeint(f, ic, t, args=([ka1, ka2, ka3], [kd1, kd2, kd3], [b1, b2, b3])) + Phi = lambda r_rel : Phi_soil[i] + (rootwateruptake[i]-waterinflow[i])*(r_rel**2*rho**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) + radial_waterflow = lambda r_rel : waterinlow[i] + (rootwateruptake[i]-waterinflow[i]) * (1 - r_rel**2) / (1-1/rho**2) + Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel), self.sp)),10/3) / (sp.theta_S**2) # Millington and Quirk + #values for the subfunction + r_crit = root_scalar(Phi, method="brentq", bracket=[1e-5, r_prhiz]) + Ds0 = Ds[i] + wateruptake_rel = (rootwateruptake[i] - waterinflow[i]) / ((r_prhiz[i]**2 - r_root[i]**2) * pi) + critical_wateruptake = rootwateruptake[i] + wateruptake_rel * ((r_root[i]**2 - r_crit[i]**2) * pi) + r_eval = [r_crit, r_root, r_prhiz] - - return rsc, Uptake + #use the subfunction #TODO: alternative lookup table + conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) + + #fix discrepancy between critical and root radius + conc_rel_c = conc_rel_c[1:]/conc_rel_c[1] + conc_mean_c = (conc_mean_c[1:]*(r_prhiz**2-r_crit**2)-conc_mean_c[1]*(r_root**2-r_crit*2))/(r_prhiz**2-r_root**2) + inflow_rel_c = inflow_rel_c[1:]/inflow_rel_c[1] + inflow_mean_c = (inflow_mean_c[1:]*(r_prhiz**2-r_crit**2)-inflow_mean_c[1]*(r_root**2-r_crit*2))/(r_prhiz**2-r_root**2) + Uptake_rel_c = Uptake_rel_c[1:]/Uptake_rel_c[1] + Uptake_mean_c = (Uptake_mean_c[1:]*(r_prhiz**2-r_crit**2)-Uptake_mean_c[1]*(r_root**2-r_crit*2))/(r_prhiz**2-r_root**2) + + #default prefactors for the steady state case + pre_c = 0 + pre_srUptake = 1 + pre_inflow = 0 + absolute = 0 + #pre_c * c(r_prhiz) + pre_srUptake * srUptake + pre_inflow * inflow = absolute + + match mode: + case "ss": #steady state + pre_c, pre_srUptake, pre_inflow, absolute = 0, 1, 0, 0 + case "sr_nf": #steady rate uptake, no influx + pre_c, pre_srUptake, pre_inflow, absolute = 0, 0, 1, 0 + case "sr_ff": #far field approximation # TODO insert link to Tiina Roose publication + #E_1(x)=int(x,inf) exp(-y)/y dy = -exp(x) - int(x,inf) -exp(-y) -exp( + #c(r_prhiz) = c_inf + B*E(r**2) + #inflow = Ds * B * exp(-r_prhiz)/r_prhiz + waterflow * c(r_prhiz) + #c_inf = outer c ? + # outer c = c_inf + B*mean(E)(2*r**2,r**2) + + # B = (outer_c - c(r_prhiz)) / (mean(E)-E) + # mean(E)(2*r**2,r**2)=E(r**2)+int(E(x)-E(r**2),r**2,2*r**2)=E(r**2)-int(r**2,2*r**2,y*exp(-y)/y)=E(r**2)+exp(-2*r**2)-exp(r**2) + # B = (outer_c - c(r_prhiz)) / (exp(-2*r**2)-exp(-r**2)) + # this means inflow can be computed + B_abs = c_outer / (math.exp(-2*r_prhiz**2)-math.exp(-r_prhiz**2)) + B_cprhiz = -1 / (math.exp(-2*r_prhiz**2)-math.exp(-r_prhiz**2)) + + pre_inflow = 1 + pre_c = - Ds_func(r_prhiz) * B_cprhiz * math.exp(-r_prhiz**2) / r_prhiz**2 - waterinflow + absolute = Ds_func(r_prhiz) * B_abs * math.exp(-r_prhiz**2) / r_prhiz**2 + pre_srUptake = 0 + case _: #default + pre_c, pre_srUptake, pre_inflow, absolute = 0, 1, 0, 0 + #equation + #c11 * ss flow + c12 * sr uptake + c13 * rsc = c_mean + #c21 * ss flow + c22 * sr uptake + c23 * rsc = c_prhiz + #ss flow + sr uptake = Uptake = f(rsc) + #pre_c * c(r_prhiz) + pre_srUptake * srUptake + pre_inflow * inflow (= ssflow) = absolute + #pre_c * (c21 * ss flow + c22 * sr uptake + c23 * rsc) + pre_srUptake * srUptake + pre_inflow * inflow = absolute + + #variables to eliminate: ss flow, sr uptake, c_prhiz, Uptake + # variable for the final equations: rsc + A = np.zeros((4,4)) + b = np.zeros((4,2)) #right hand side of the linear equation, one for absolute values, one for the rsc value + + #c11 * ss flow + c12 * sr uptake + c13 * rsc = c_mean + A[0,0] = inflow_mean_c + A[0,1] = Uptake_mean_c + b[0,1] = -conc_mean_c + b[0,0] = c_soil + + #c21 * ss flow + c22 * sr uptake + c23 * rsc = c_prhiz + A[1,0] = inflow_rel_c + A[1,1] = Uptake_rel_c + A[1,2] = -1 + b[1,1] = -conc_rel_c + + #ss flow + sr uptake = Uptake + A[2,0] = 1 + A[2,1] = 1 + A[2,3] = -1 + + #pre_c * c(r_prhiz) + pre_srUptake * srUptake + pre_inflow * inflow (= ssflow) = absolute + A[3,0] = pre_inflow + A[3,1] = pre_srUptake + A[3,2] = pre_c1 + b[3,0] = absolute + + #m*rsc+n = [ss uptake, sr uptake, c(rprhiz), Uptake] + n = la.solve(A,b[:,0]) + m = la.solve(A,b[:,1]) + + #solve quadratic equation + #Uptake = Vmax * rsc / (Km + rsc) + #(Km + rsc) * (m[3]*rsc+n[3]) = Vmax * rsc + # m[3] * rsc**2 + (Km[i]*m[3]+n[3]-Vmax[i]) * rsc + Km[i]*n[3] = 0 + + if abs(m[3]) Date: Sun, 14 Jun 2026 15:11:09 +0200 Subject: [PATCH 20/41] ploting ss, sr and constant --- src/functional/Perirhizal.py | 65 +++++++++++++++++++++++------------- test/test_perirhizal.py | 26 +++++++++++++++ 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 13ef9bc1a..f7f367884 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -420,10 +420,24 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, #linear equations for c_prhiz, rsc, ss_flow, sr_uptake, Uptake (4 linear, 1 quadratic) return rsc, Uptake, ss_uptake, sr_uptake - def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_bulk, c_root, Ds0, ss_uptake, sr_uptake, sp, N): + def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, r_eval, c_root, Ds0, ss_uptake, sr_uptake, sp): """ given the water and solute uptake data this computes the discretisation of the steady rate solutions + Phi_soil outer matrix flux potential [cm2/d] + rootwateruptake radial root water uptake [cm2/d] + waterinflow radial water inflow at r_prhiz [cm2/d] + r_root root radius [cm] + r_prhiz outer radius [cm] + r_eval positions at which the solute concentration should be evaluated [cm] + c_root solute concentration next to the root [mol/cm3] + Ds0 Diffusion constant in water [cm2/d] + ss_uptake outer inflow of solutes [mol/(cm d)] + sr_uptake solute uptake from the perirhizal zone [mol/(cm3d)] + sp van Genuchten parameter set + + output: + watercontent, waterpotential, soluteconcentration discretisations """ #initialize the lambda functions @@ -435,31 +449,31 @@ def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_pr rho = r_prhiz / r_root #reference radius for the solute implementation is either at the perirhizal radius (steady state water uptake) or at the radius without wateruptake #in both cases it is scaled by Ds0 / self.Ds0 - r_rel0 = r_prhiz * Ds0 / self.Ds0 + #r_rel0 = r_prhiz * Ds0 / self.Ds0 #water is easy - Phi = lambda r : Phi_soil + (rootwateruptake - waterinflow) * ((r_rel/r_root)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(r_prhiz/r)-0.5)) + waterinflow * np.log(r / r_prhiz) - waterpotential = lambda r : vg.fast_imfp(Phi(r),sp) - watercontent = lambda r : vg.watercontent(Phi(r),sp) + Phi = lambda r : Phi_soil + (rootwateruptake - waterinflow) * ((r/r_root)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(r_prhiz/r)-0.5)) + waterinflow * np.log(r / r_prhiz) + waterpotential = lambda r : vg.fast_imfp[sp](Phi(r)) + watercontent = lambda r : vg.watercontent[sp](waterpotential(r)) - r_crit = root_scalar(Phi, method="brentq", bracket=[1e-5, r_prhiz]) + print("Phi_root",Phi(1e-10), Phi(r_prhiz)) + r_crit = root_scalar(Phi, method="brentq", bracket=[1e-10,r_prhiz]).root #solutes - r_eval = [r_crit, np.linspace(r_root,r_prhiz,N)] + #r_eval = [r_crit, r_eval] + r_eval = np.concatenate(([r_crit],r_eval)) #use the subfunction #TODO: alternative lookup table - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) + wateruptake_rel = (rootwateruptake - waterinflow) / ((r_prhiz**2 - r_root**2) * np.pi) + critical_wateruptake = rootwateruptake + wateruptake_rel * (r_root**2 - r_crit**2) * np.pi + + conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) soluteconcentration = c_root * conc_rel_c[1:] + ss_uptake * inflow_rel_c[1:] + sr_uptake * Uptake_rel_c[1:] - #check if there is a lookup table - if self.lookup_table_sr_solutes: - c_solutes = lambda r: self.lookup_table_sr_solutes((Phi_ref, wateruptake_rel, [r])) - else: - abc return watercontent, waterpotential, soluteconcentration - def solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp): + def solute_linearequation_sr_(self, Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp): """ computes a linear equation from the diffusion advection ODE on the perirhizal solute flow #TODO: insert equation number @@ -497,24 +511,27 @@ def solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval #waterflow Phi = lambda r_rel : wateruptake_rel*(r_rel**2/2 - r_crit**2/2 - np.log(r_rel / r_crit)) #[cm2/d] - radial_waterflow = lambda r_rel : critical_wateruptake - wateruptake_rel*(r_rel**2 - r_crit**2)*pi + radial_waterflow = lambda r_rel : - (critical_wateruptake - wateruptake_rel*(r_rel**2 - r_crit**2)*np.pi) watercontent = lambda r_rel : vg.water_content(vg.fast_imfp[sp](Phi(r_rel)), sp) Ds = lambda r_rel : Ds0 * math.pow(watercontent(r_rel),10/3) / (sp.theta_S**2) - f_homogen = lambda r_rel, c : ( - radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference - f_steadystate = lambda r_rel, c : (1 - radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference - f_steadyrate = lambda r_rel, c : ((r_rel**2 - r_crit**2)*pi - radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference + f_homogen = lambda c, r_rel : ( radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference + f_steadystate = lambda c, r_rel : (1 + radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference + f_steadyrate = lambda c, r_rel : ((1**2-r_rel**2)*np.pi + radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference #numerically solve the ODE c' = f(r_rel,c) starting at r_rel=1 - conc_rel_c = solve_ivp(f_homogen, t_span = [r_crit, r_eval[-1]], 1, t_eval = r_eval) - inflow_rel_c = solve_ivp(f_steadystate, t_span = [r_crit, r_eval[-1]], 0, t_eval = r_eval) - Uptake_rel_c = solve_ivp(f_steadyrate, t_span = [r_crit, r_eval[-1]], 0, t_eval = r_eval) + conc_rel_c = odeint(f_homogen, y0 = 1, t = r_eval) + conc_rel_c = np.array([element[0] for element in conc_rel_c]) + inflow_rel_c = odeint(f_steadystate, y0 = 0, t = r_eval) + inflow_rel_c = np.array([element[0] for element in inflow_rel_c]) + Uptake_rel_c = odeint(f_steadyrate, y0 = 0, t = r_eval) + Uptake_rel_c = np.array([element[0] for element in Uptake_rel_c]) #compute the means watercontent_times_r = np.array([watercontent(r_rel)*r_rel for r_rel in r_eval]) - conc_mean_c = np.array([np.average(conc_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(n)]) - inflow_mean_c = np.array([np.average(inflow_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(n)]) - Uptake_mean_c = np.array([np.average(Uptake_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(n)]) + conc_mean_c = np.array([np.average(conc_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) + inflow_mean_c = np.array([np.average(inflow_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) + Uptake_mean_c = np.array([np.average(Uptake_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 802d80732..989880639 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -77,6 +77,8 @@ def run_perirhizal_test(): + global r_root, r_prhiz + #space for the outputs water_dumux = np.zeros((n_times, n_sp+1)) water_sr2 = np.zeros((n_times, n_sp+1)) @@ -129,6 +131,30 @@ def run_perirhizal_test(): sp = vg.Parameters(soilVG) peri.set_soil(sp) #no lookup tables are used here as there arent many simulations + + + #test the plots + Phi_soil = vg.fast_mfp[peri.sp](-100) + r_root = 0.02 + r_prhiz = 1 + r_eval = np.linspace(r_root, r_prhiz, 100) + rootwateruptake = 0.2 + waterinflow = 0.1 + c_root = 0* 1e-7 + Ds0 = Ds + ss_uptake = 0*1.e-7 + sr_uptake = 1*1.e-7 + watercontent, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, r_eval, c_root, Ds0, ss_uptake, sr_uptake, sp) + print("watercontent", watercontent, "waterpotential", waterpotential, "soluteconcentration", soluteconcentration) + + plt.plot(r_eval, soluteconcentration) + + plt.xlabel('X-axis') + plt.ylabel('Y-axis') + plt.title('Simple Plot') + + plt.show() + return 0 outer_watercontent = vg.water_content(outer_waterpotential, peri.sp) From 28a6611c795a21ef8200ae3ffb9d25e7dfaa2a68 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sun, 14 Jun 2026 20:58:01 +0200 Subject: [PATCH 21/41] fix sign error plot sr_nf --- src/functional/Perirhizal.py | 54 ++++++++++++++------------ test/test_perirhizal.py | 74 +++++++++++++++++++++--------------- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index f7f367884..7d6b2e438 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -263,7 +263,7 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, return rsc - def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_bulk, Vmax, Km, Ds, sp, mode = "sr_ff"): + def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km, Ds, sp, mode = "sr_ff"): """ steady state assumption of solute uptake by roots TODO: insert citaiton @@ -285,9 +285,9 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, Uptake solute uptake of the root [mol/(cm d)] """ - assert len(Phi_A) == len(Phi_C) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" + assert len(Phi_soil) == len(rootwateruptake) == len(waterinflow) == len(r_root) == len(r_prhiz) == len(c_soil) == len(c_outer) == len(Vmax) == len(Km) == len(Ds), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" - n_segments = len(c_bulk) + n_segments = len(c_soil) rsc = np.zeros(n_segments) Uptake = np.zeros(n_segments) @@ -296,27 +296,29 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): + rho = r_prhiz[i] / r_root[i] Phi = lambda r_rel : Phi_soil[i] + (rootwateruptake[i]-waterinflow[i])*(r_rel**2*rho**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) radial_waterflow = lambda r_rel : waterinlow[i] + (rootwateruptake[i]-waterinflow[i]) * (1 - r_rel**2) / (1-1/rho**2) - Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel), self.sp)),10/3) / (sp.theta_S**2) # Millington and Quirk + Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel)),sp),10/3) / (sp.theta_S**2) # Millington and Quirk #values for the subfunction - r_crit = root_scalar(Phi, method="brentq", bracket=[1e-5, r_prhiz]) + r_crit = root_scalar(Phi, method="brentq", bracket=[1e-5, r_prhiz[i]]).root Ds0 = Ds[i] - wateruptake_rel = (rootwateruptake[i] - waterinflow[i]) / ((r_prhiz[i]**2 - r_root[i]**2) * pi) - critical_wateruptake = rootwateruptake[i] + wateruptake_rel * ((r_root[i]**2 - r_crit[i]**2) * pi) - r_eval = [r_crit, r_root, r_prhiz] + wateruptake_rel = (rootwateruptake[i] - waterinflow[i]) / ((r_prhiz[i]**2 - r_root[i]**2) * np.pi) + critical_wateruptake = rootwateruptake[i] + wateruptake_rel * ((r_root[i]**2 - r_crit**2) * np.pi) + r_eval = [r_crit, r_root[i], r_prhiz[i]] #use the subfunction #TODO: alternative lookup table - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) + conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) #fix discrepancy between critical and root radius + #TODO: turn the means into floats instead of lists with one entry conc_rel_c = conc_rel_c[1:]/conc_rel_c[1] - conc_mean_c = (conc_mean_c[1:]*(r_prhiz**2-r_crit**2)-conc_mean_c[1]*(r_root**2-r_crit*2))/(r_prhiz**2-r_root**2) + conc_mean_c = (conc_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-conc_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) inflow_rel_c = inflow_rel_c[1:]/inflow_rel_c[1] - inflow_mean_c = (inflow_mean_c[1:]*(r_prhiz**2-r_crit**2)-inflow_mean_c[1]*(r_root**2-r_crit*2))/(r_prhiz**2-r_root**2) + inflow_mean_c = (inflow_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-inflow_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) Uptake_rel_c = Uptake_rel_c[1:]/Uptake_rel_c[1] - Uptake_mean_c = (Uptake_mean_c[1:]*(r_prhiz**2-r_crit**2)-Uptake_mean_c[1]*(r_root**2-r_crit*2))/(r_prhiz**2-r_root**2) + Uptake_mean_c = (Uptake_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-Uptake_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) #default prefactors for the steady state case pre_c = 0 @@ -341,12 +343,12 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, # mean(E)(2*r**2,r**2)=E(r**2)+int(E(x)-E(r**2),r**2,2*r**2)=E(r**2)-int(r**2,2*r**2,y*exp(-y)/y)=E(r**2)+exp(-2*r**2)-exp(r**2) # B = (outer_c - c(r_prhiz)) / (exp(-2*r**2)-exp(-r**2)) # this means inflow can be computed - B_abs = c_outer / (math.exp(-2*r_prhiz**2)-math.exp(-r_prhiz**2)) - B_cprhiz = -1 / (math.exp(-2*r_prhiz**2)-math.exp(-r_prhiz**2)) + B_abs = c_outer[i] / (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) + B_cprhiz = -1 / (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) pre_inflow = 1 - pre_c = - Ds_func(r_prhiz) * B_cprhiz * math.exp(-r_prhiz**2) / r_prhiz**2 - waterinflow - absolute = Ds_func(r_prhiz) * B_abs * math.exp(-r_prhiz**2) / r_prhiz**2 + pre_c = - Ds_func(r_prhiz[i]) * B_cprhiz * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 - waterinflow[i] + absolute = Ds_func(r_prhiz[i]) * B_abs * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 pre_srUptake = 0 case _: #default pre_c, pre_srUptake, pre_inflow, absolute = 0, 1, 0, 0 @@ -363,16 +365,16 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, b = np.zeros((4,2)) #right hand side of the linear equation, one for absolute values, one for the rsc value #c11 * ss flow + c12 * sr uptake + c13 * rsc = c_mean - A[0,0] = inflow_mean_c - A[0,1] = Uptake_mean_c - b[0,1] = -conc_mean_c - b[0,0] = c_soil + A[0,0] = inflow_mean_c[0] + A[0,1] = Uptake_mean_c[0] + b[0,1] = -conc_mean_c[0] + b[0,0] = c_soil[i] #c21 * ss flow + c22 * sr uptake + c23 * rsc = c_prhiz - A[1,0] = inflow_rel_c - A[1,1] = Uptake_rel_c + A[1,0] = inflow_rel_c[0] + A[1,1] = Uptake_rel_c[0] A[1,2] = -1 - b[1,1] = -conc_rel_c + b[1,1] = -conc_rel_c[0] #ss flow + sr uptake = Uptake A[2,0] = 1 @@ -382,7 +384,7 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, #pre_c * c(r_prhiz) + pre_srUptake * srUptake + pre_inflow * inflow (= ssflow) = absolute A[3,0] = pre_inflow A[3,1] = pre_srUptake - A[3,2] = pre_c1 + A[3,2] = pre_c b[3,0] = absolute #m*rsc+n = [ss uptake, sr uptake, c(rprhiz), Uptake] @@ -394,6 +396,8 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, #(Km + rsc) * (m[3]*rsc+n[3]) = Vmax * rsc # m[3] * rsc**2 + (Km[i]*m[3]+n[3]-Vmax[i]) * rsc + Km[i]*n[3] = 0 + tol = 1e-9 #arbitrary for now, TODO think of a reasonable tolerance + if abs(m[3]) Date: Mon, 15 Jun 2026 08:24:27 +0200 Subject: [PATCH 22/41] plotting the generalised soluteuptake --- src/functional/Perirhizal.py | 7 ++- test/test_perirhizal.py | 103 ++++++++++++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 7d6b2e438..3dd78e122 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -297,6 +297,7 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): rho = r_prhiz[i] / r_root[i] + print("rootwateruptake",rootwateruptake[i],waterinflow[i]) Phi = lambda r_rel : Phi_soil[i] + (rootwateruptake[i]-waterinflow[i])*(r_rel**2*rho**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) radial_waterflow = lambda r_rel : waterinlow[i] + (rootwateruptake[i]-waterinflow[i]) * (1 - r_rel**2) / (1-1/rho**2) Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel)),sp),10/3) / (sp.theta_S**2) # Millington and Quirk @@ -457,8 +458,8 @@ def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_pr #water is easy Phi = lambda r : Phi_soil + (rootwateruptake - waterinflow) * ((r/r_root)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(r_prhiz/r)-0.5)) + waterinflow * np.log(r / r_prhiz) - waterpotential = lambda r : vg.fast_imfp[sp](Phi(r)) - watercontent = lambda r : vg.watercontent[sp](waterpotential(r)) + waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) + watercontent_func = lambda r : vg.water_content(waterpotential(r),sp) print("Phi_root",Phi(1e-10), Phi(r_prhiz)) r_crit = root_scalar(Phi, method="brentq", bracket=[1e-10,r_prhiz]).root @@ -474,6 +475,8 @@ def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_pr soluteconcentration = c_root * conc_rel_c[1:] + ss_uptake * inflow_rel_c[1:] + sr_uptake * Uptake_rel_c[1:] + watercontent = np.array([watercontent_func(r) for r in r_eval]) + waterpotential = np.array([waterpotential_func(r) for r in r_eval]) return watercontent, waterpotential, soluteconcentration diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 86087fa9d..2c8e3527b 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -29,7 +29,7 @@ # run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model n_tests = 1 #try everything here for this many random parameter sets -do_computation = True #should the computation be run or take the data from a saved file +do_computation = False #should the computation be run or take the data from a saved file # general parameters @@ -59,6 +59,13 @@ matrixpotential_perirhizal1 = np.zeros((n_tests, n_times, n_sp+1)) matrixpotential_perirhizal2 = np.zeros((n_tests, n_times, n_sp+1)) +general_matrixpotential = np.zeros((n_tests, n_times, n_sp+1)) +general_water = np.zeros((n_tests, n_times, n_sp+1)) + +general_ss = np.zeros((n_tests, n_times, n_sp+1)) +general_sr = np.zeros((n_tests, n_times, n_sp+1)) +general_ff = np.zeros((n_tests, n_times, n_sp+1)) + soluteconcentration_Tiina = np.zeros((n_tests, n_times, n_sp+1)) soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil @@ -89,6 +96,9 @@ def run_perirhizal_test(): water_test = np.zeros((n_times, n_sp+1)) solute_srnf = np.zeros((n_times, n_sp+1)) + general_water = np.zeros((n_times, n_sp+1)) + general_waterpotential = np.zeros((n_times, n_sp+1)) + general_ss = np.zeros((n_times, n_sp+1)) general_sr = np.zeros((n_times, n_sp+1)) general_ff = np.zeros((n_times, n_sp+1)) @@ -141,7 +151,7 @@ def run_perirhizal_test(): Phi_soil = vg.fast_mfp[peri.sp](-100) r_root = 0.02 r_prhiz = 1 - r_eval = np.linspace(r_root, r_prhiz, 100) + r_eval = CC#np.linspace(r_root, r_prhiz, 100) rootwateruptake = 0.2 waterinflow = 0.1 c_root = 0* 1e-7 @@ -265,6 +275,8 @@ def run_perirhizal_test(): soilSoilFluxes = s.getOuterFlow(0, length) * dt # cm3 soilSoilFluxesS = s.getOuterFlow(1, length) * dt # mol + rootwateruptake = abs(rootSoilFluxes) / dt + waterinflow = np.array([abs(outer_watersource * volumes[-1])]) @@ -412,14 +424,36 @@ def run_perirhizal_test(): print("F_tilde", F_tilde) solute_ss[r,j+1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[r,0] / abs(waterdemand)#waterdemand is assumed to be negative solute_srnf[r,j+1] = result_solutes_srnf * F_tilde + (1-F_tilde) * solute_srnf[r,0] / abs(waterdemand)#waterdemand is assumed to be negative - + solutes_Tiina[r,0] = peri.solutesuptake_convdiff_([mean_water],[outer_conc], [Vmax], [Km], Ds, [waterflow], [r_root], [0.], [time/1.5], peri.sp)[0] - return water_dumux, solute_dumux, solute_ss, water_sr, solute_srnf, water_sr2, mp_perirhizal1, mp_perirhizal2, solutes_Tiina, general_ss, general_sr, general_ff + + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ff") + watercontent, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_soil, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + + general_water[r,:] = watercontent[:] + general_waterpotential[r,:] = waterpotential[:] + general_ff[r,1:] = soluteconcentration[:] + + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ss") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + general_ss[r,1:] = soluteconcentration[:] + + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_sr") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + general_sr[r,1:] = soluteconcentration[:] + return water_dumux, solute_dumux, solute_ss, water_sr, solute_srnf, water_sr2, mp_perirhizal1, mp_perirhizal2, solutes_Tiina, general_water, general_waterpotential, general_ss, general_sr, general_ff + +general_waterpotential_plot = np.zeros((n_tests, n_times, n_sp+1)) +general_water_plot = np.zeros((n_tests, n_times, n_sp+1)) + +general_ss_plot = np.zeros((n_tests, n_times, n_sp+1)) +general_sr_plot = np.zeros((n_tests, n_times, n_sp+1)) +general_ff_plot = np.zeros((n_tests, n_times, n_sp+1)) if do_computation: # save everything in the np arrays for i in range(n_tests): - water_dumux, solute_dumux, solute_ss, water_sr, solute_srnf, water_sr2, mp_perirhizal1, mp_perirhizal2, solutes_Tiina, general_ss, general_sr, general_ff = run_perirhizal_test() + water_dumux, solute_dumux, solute_ss, water_sr, solute_srnf, water_sr2, mp_perirhizal1, mp_perirhizal2, solutes_Tiina, general_water, general_waterpotential, general_ss, general_sr, general_ff = run_perirhizal_test() watercontent_dumux[i,:,:]=water_dumux[:,:] soluteconcentration_dumux[i,:,:]=solute_dumux[:,:] soluteconcentration_steadystate[i,:,:]=solute_ss[:,:] @@ -429,8 +463,14 @@ def run_perirhizal_test(): matrixpotential_perirhizal1[i,:,:]=mp_perirhizal1[:,:] matrixpotential_perirhizal2[i,:,:]=mp_perirhizal2[:,:] soluteconcentration_Tiina[i,:,:]=solutes_Tiina[:,:] + + general_waterpotential_plot[i,:,:]=general_waterpotential[:,:] + general_water_plot[i,:,:]=general_water[:,:] + general_ss_plot[i,:,:]=general_ss[:,:] + general_sr_plot[i,:,:]=general_sr[:,:] + general_ff_plot[i,:,:]=general_ff[:,:] - np.savez("test_perirhizal.npz", watercontent_dumux=watercontent_dumux, soluteconcentration_Tiina=soluteconcentration_Tiina, watercontent_steadyrate2=watercontent_steadyrate2, matrixpotential_perirhizal1=matrixpotential_perirhizal1, matrixpotential_perirhizal2=matrixpotential_perirhizal2, soluteconcentration_dumux=soluteconcentration_dumux, soluteconcentration_steadystate=soluteconcentration_steadystate, watercontent_steadyrate=watercontent_steadyrate, soluteconcentration_steadyrate=soluteconcentration_steadyrate) + np.savez("test_perirhizal.npz", general_waterpotential_plot=general_waterpotential_plot,general_water_plot=general_water_plot,general_ss_plot=general_ss_plot,general_sr_plot=general_sr_plot,general_ff_plot=general_ff_plot,watercontent_dumux=watercontent_dumux, soluteconcentration_Tiina=soluteconcentration_Tiina, watercontent_steadyrate2=watercontent_steadyrate2, matrixpotential_perirhizal1=matrixpotential_perirhizal1, matrixpotential_perirhizal2=matrixpotential_perirhizal2, soluteconcentration_dumux=soluteconcentration_dumux, soluteconcentration_steadystate=soluteconcentration_steadystate, watercontent_steadyrate=watercontent_steadyrate, soluteconcentration_steadyrate=soluteconcentration_steadyrate) else: simulation_results = np.load("test_perirhizal.npz") @@ -443,6 +483,11 @@ def run_perirhizal_test(): matrixpotential_perirhizal1 = simulation_results["matrixpotential_perirhizal1"] matrixpotential_perirhizal2 = simulation_results["matrixpotential_perirhizal2"] soluteconcentration_Tiina = simulation_results["soluteconcentration_Tiina"] + general_waterpotential_plot = simulation_results["general_waterpotential_plot"] + general_water_plot = simulation_results["general_water_plot"] + general_ss_plot = simulation_results["general_ss_plot"] + general_sr_plot = simulation_results["general_sr_plot"] + general_ff_plot = simulation_results["general_ff_plot"] @@ -541,8 +586,10 @@ def run_perirhizal_test(): #ax1[i].plot(CC, mp_steadyrate2, "b", linestyle = linestyle_steadystate, label = "water_sr_stress") ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") #ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") - ax2.plot(CC, solute_steadyrate * solute_dumux[-1] / solute_steadyrate[-1], "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - ax2.plot(CC, solute_steadystate * solute_dumux[10] / solute_steadystate[10], "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") + ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") + #ax2.plot(CC, solute_steadyrate * solute_dumux[-1] / solute_steadyrate[-1], "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + #ax2.plot(CC, solute_steadystate * solute_dumux[10] / solute_steadystate[10], "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") #ax2.plot(CC, solute_Tiina, "m", linestyle = linestyle_steadystate, label = "solute_Tiina") ax2.plot(CC, solute_Tiina * solute_dumux[-1] / outer_conc, "m", linestyle = linestyle_special, label = "solute_Tiina_scaled") @@ -555,6 +602,46 @@ def run_perirhizal_test(): ax1[i,0].legend(loc="upper left") ax2.legend(loc="upper right") #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +#plt.show() + +for i in range(5): + ax2 = ax1[i,1].twinx() + + water_dumux = watercontent_dumux[run, timestep[i], 1:] + water_general = general_waterpotential_plot[run, timestep[i], 1:] + solute_dumux = soluteconcentration_dumux[run, timestep[i], 1:] + solute_steadystate = general_ss_plot[run, timestep[i], 1:] + solute_steadyrate = general_sr_plot[run, timestep[i], 1:] + solute_farfield = general_ff_plot[run, timestep[i], 1:] + + #mp_steadyrate1 = matrixpotential_perirhizal1[run, timestep[i], 1:] + #mp_steadyrate2 = matrixpotential_perirhizal2[run, timestep[i], 1:] + + + water_dumux = vg.pressure_head(water_dumux, peri.sp) + #water_perirhizal = vg.pressure_head(water_perirhizal, peri.sp) + #water_perirhizal2 = vg.pressure_head(water_steadyrate2, peri.sp) + + ax1[i,1].plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") + ax1[i,1].plot(CC, water_general, "b", linestyle = linestyle_steadyrate, label = "water_general") #this and the following line give the same results, as they should + ax1[i,1].plot(CC, mp_steadyrate1, "b", linestyle = linestyle_steadyrate, label = "water_sr") + #ax1[i].plot(CC, mp_steadyrate2, "b", linestyle = linestyle_steadystate, label = "water_sr_stress") + ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") + #ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") + ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") + ax2.plot(CC, solute_farfield, "m", linestyle = linestyle_special, label = "solute_farfield") + #ax2.plot(CC, solute_Tiina * solute_dumux[-1] / outer_conc, "m", linestyle = linestyle_special, label = "solute_Tiina_scaled") + + ax1[i,1].set_xlabel("distance root [cm]") + ax1[i,1].set_ylabel("water") + ax2.set_ylabel("nitrogen") + ax1[i,1].legend(["watercontent cm3/cm3"], loc="upper left") + ax2.legend(["nitrogen concentration mol/cm3"], loc="upper right") + + ax1[i,1].legend(loc="upper left") + ax2.legend(loc="upper right") +#np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) plt.show() # for i in range(5): From 94d7966f8a5cb0e91f52012ed344f3da129c1240 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Fri, 19 Jun 2026 15:06:27 +0200 Subject: [PATCH 23/41] ss and sr produce realistic distributions --- src/functional/Perirhizal.py | 76 +++++++++++++-------- test/test_perirhizal.py | 128 +++++++++++++++++++---------------- 2 files changed, 118 insertions(+), 86 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 3dd78e122..5b95c60a5 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -7,6 +7,7 @@ from scipy.integrate import odeint from scipy import integrate +import time #only used for a test import math @@ -298,12 +299,18 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, for i in range(n_segments): rho = r_prhiz[i] / r_root[i] print("rootwateruptake",rootwateruptake[i],waterinflow[i]) - Phi = lambda r_rel : Phi_soil[i] + (rootwateruptake[i]-waterinflow[i])*(r_rel**2*rho**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) - radial_waterflow = lambda r_rel : waterinlow[i] + (rootwateruptake[i]-waterinflow[i]) * (1 - r_rel**2) / (1-1/rho**2) + Phi_out = Phi_soil[i] -( (rootwateruptake[i]-waterinflow[i])*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow[i]*np.log(0.53) ) + #Phi_out = root_scalar(Phi_diff, method="brentq", bracket=[1e-5, Phi_soil]).root + Phi = lambda r_rel : Phi_out + (rootwateruptake[i]-waterinflow[i])*((r_rel**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) #TODO: test using Phi_soil instead of Phi_outer + radial_waterflow = lambda r_rel : waterinflow[i] + (rootwateruptake[i]-waterinflow[i]) * (1 - r_rel**2) / (1-1/rho**2) Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel)),sp),10/3) / (sp.theta_S**2) # Millington and Quirk #values for the subfunction - r_crit = root_scalar(Phi, method="brentq", bracket=[1e-5, r_prhiz[i]]).root + print("Phi", Phi(1e-10), Phi(r_prhiz[i])) + # = root_scalar(Phi, method="brentq", bracket=[1e-10, r_prhiz[i]]).root + + r_crit = r_root[i] / 100 + Ds0 = Ds[i] wateruptake_rel = (rootwateruptake[i] - waterinflow[i]) / ((r_prhiz[i]**2 - r_root[i]**2) * np.pi) critical_wateruptake = rootwateruptake[i] + wateruptake_rel * ((r_root[i]**2 - r_crit**2) * np.pi) @@ -341,11 +348,11 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, # outer c = c_inf + B*mean(E)(2*r**2,r**2) # B = (outer_c - c(r_prhiz)) / (mean(E)-E) - # mean(E)(2*r**2,r**2)=E(r**2)+int(E(x)-E(r**2),r**2,2*r**2)=E(r**2)-int(r**2,2*r**2,y*exp(-y)/y)=E(r**2)+exp(-2*r**2)-exp(r**2) - # B = (outer_c - c(r_prhiz)) / (exp(-2*r**2)-exp(-r**2)) + # mean(E)(2*r**2,r**2)=E(r**2)+int(E(x)-E(r**2),r**2,2*r**2) / r**2=E(r**2)-int(r**2,2*r**2,y*exp(-y)/y) / r**2=E(r**2)+(exp(-2*r**2)-exp(r**2)) / r**2 + # B = (outer_c - c(r_prhiz)) * (r_prhiz**2) / (exp(-2*r**2)-exp(-r**2)) # this means inflow can be computed - B_abs = c_outer[i] / (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) - B_cprhiz = -1 / (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) + B_abs = c_outer[i] * (r_prhiz[i]**2)/ (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) + B_cprhiz = -1 * (r_prhiz[i]**2) / (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) pre_inflow = 1 pre_c = - Ds_func(r_prhiz[i]) * B_cprhiz * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 - waterinflow[i] @@ -402,12 +409,15 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, if abs(m[3])0: + rsc[i] = - p/2 + math.sqrt(p**2/4-q) #only the positive solution makes sense #test for negative + else: + rsc[i] = - p/2 - math.sqrt(p**2/4-q) #this is the standard case as m[3] will usually be negative Uptake[i] = m[3]*rsc[i]+n[3] ss_uptake[i] = m[0]*rsc[i]+n[0] sr_uptake[i] = m[1]*rsc[i]+n[1] @@ -445,10 +455,6 @@ def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_pr watercontent, waterpotential, soluteconcentration discretisations """ - #initialize the lambda functions - watercontent = lambda r : 0 - waterpotential = lambda r : 0 - soluteconcentration = lambda r : 0 #simple computaitons rho = r_prhiz / r_root @@ -457,13 +463,15 @@ def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_pr #r_rel0 = r_prhiz * Ds0 / self.Ds0 #water is easy - Phi = lambda r : Phi_soil + (rootwateruptake - waterinflow) * ((r/r_root)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(r_prhiz/r)-0.5)) + waterinflow * np.log(r / r_prhiz) + Phi_out = Phi_soil -( (rootwateruptake-waterinflow)*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow*np.log(0.53) ) + Phi = lambda r : Phi_out + (rootwateruptake - waterinflow) * ((r/r_root)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(r_prhiz/r)-0.5)) + waterinflow * np.log(r / r_prhiz) + Phi = lambda r : Phi_soil #TODO: remove test waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) - watercontent_func = lambda r : vg.water_content(waterpotential(r),sp) + watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) print("Phi_root",Phi(1e-10), Phi(r_prhiz)) - r_crit = root_scalar(Phi, method="brentq", bracket=[1e-10,r_prhiz]).root - + #r_crit = root_scalar(Phi, method="brentq", bracket=[1e-10,r_prhiz]).root + r_crit = r_root / 100 #solutes #r_eval = [r_crit, r_eval] r_eval = np.concatenate(([r_crit],r_eval)) @@ -519,7 +527,8 @@ def solute_linearequation_sr_(self, Ds0, critical_wateruptake, wateruptake_rel, watercontent_times_r = np.ones(n) #waterflow - Phi = lambda r_rel : wateruptake_rel*(r_rel**2/2 - r_crit**2/2 - np.log(r_rel / r_crit)) #[cm2/d] + Phi = lambda r_rel : wateruptake_rel*(r_rel**2/2 - r_crit**2/2 - np.log(r_rel / r_crit)) + (critical_wateruptake - wateruptake_rel *(1-r_crit**2)*np.pi)*np.log(r_rel)#[cm2/d] + Phi = lambda r_rel : vg.fast_imfp[sp](-200) radial_waterflow = lambda r_rel : - (critical_wateruptake - wateruptake_rel*(r_rel**2 - r_crit**2)*np.pi) watercontent = lambda r_rel : vg.water_content(vg.fast_imfp[sp](Phi(r_rel)), sp) Ds = lambda r_rel : Ds0 * math.pow(watercontent(r_rel),10/3) / (sp.theta_S**2) @@ -592,9 +601,9 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf F_tilde_inv = np.zeros(n_segments) if self.lookup_table_solutes: - F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, len(c_bulk))] + F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, n_segments)] else: - F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] + F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, n_segments)] #compute a prefactor D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) @@ -608,7 +617,7 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf a2=(1-F_tilde_inv[i])/(waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 - rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) + rsc[i]=-p/2-math.sqrt(pow(p/2,2)-q) return rsc @@ -676,7 +685,7 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w if q>0: print("q>0",Km[i],a1,F_tilde_inv[i],R_sr[i],c_bulk[i],watercontent[i]) q=0 - rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) + rsc[i]=-p/2-math.sqrt(pow(p/2,2)-q) return rsc @@ -916,15 +925,18 @@ def create_lookup(self, filename, sp): np.savez(filename, interface=interface, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, soil=list(sp)) self.lookup_table = RegularGridInterpolator((inner_kr_b_, base_mfp_), interface) self.sp = sp + + print("Done with creating a lookup table for the hydraulic perirhizal resistance model") + return 0 - def create_lookup_global(self, filename, sp): + def create_lookup_global(self, filename, dummy_sp = vg.Parameters([0.078, 0.43, 0.036, 1.56, 24.96])): """ Precomputes all soil root interface potentials for a specific soil type and saves results into a 3D lookup table filename three files are written (filename, filename_, and filename_soil) - sp van genuchten soil parameters, , call - vg.create_mfp_lookup(sp) before + dummy_sp van genuchten soil parameters set for the perirhizal zone, not needed in the actual lookup table + has been set to hydrus loam as a default """ @@ -952,7 +964,7 @@ def create_lookup_global(self, filename, sp): interface_vg = np.zeros((vg_m_n,sx_n)) #construct a smaller lookup table for the simplified matrix flux potential - print("Creating a global lookup table for the matrix flux potential") + print("Creating a general lookup table for the matrix flux potential") for i, vg_m in enumerate(vg_m_): print(i, " / ", vg_m_n) sp_dummy = vg.Parameters([0.1,0.4,self.alpha_0,1./(1.-vg_m),1.0]) @@ -988,7 +1000,10 @@ def create_lookup_global(self, filename, sp): interface[i, j, k] = PerirhizalPython.soil_root_interface_global_(self, inner_kr_b, base_mfp, vg_m) np.savez(filename, interface=interface, interface_vg=interface_vg, vg_m_=vg_m_, inner_kr_b_=inner_kr_b_, base_mfp_=base_mfp_, sx_=sx_, soil=list(sp)) self.global_lookup_table = RegularGridInterpolator((vg_m_, inner_kr_b_, base_mfp_), interface) - self.sp = sp + self.sp = dummy_sp + + print("Doen with creating a general lookup table for the matrix flux potential") + return 0 def create_integralDiffusion_lookup(self, filename, sp): """ @@ -1605,13 +1620,20 @@ def get_default_volumes_(self): # sand = [0.045, 0.43, 0.15, 3, 1000] # loam = [0.08, 0.43, 0.04, 1.6, 50] # clay = [0.1, 0.4, 0.01, 1.1, 10] + hydrus_loam = [0.078, 0.43, 0.036, 1.56, 24.96] filename = "hydrus_loam" sp = vg.Parameters(hydrus_loam) vg.create_mfp_lookup(sp) peri = PerirhizalPython() #peri.create_lookup_mpi(filename, sp) # takes some hours; mpiexec -n 4 python Perirhizal.py + start = time.time() peri.create_lookup(filename, sp) # should be better + end = time.time() + print("Creating the simplified lookup table for hydrus loam took ", end - start, " seconds.") + start = time.time() peri.create_lookup_global(peri.water_filename) # this is global + end = time.time() + print("Creating the general lookup table for all van Genuchten sets took ", end - start, " seconds.") # generate some tests diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 2c8e3527b..57809d35f 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -29,12 +29,12 @@ # run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model n_tests = 1 #try everything here for this many random parameter sets -do_computation = False #should the computation be run or take the data from a saved file +do_computation = True #should the computation be run or take the data from a saved file # general parameters max_time = 1 #d -n_times = 20 # number of time intervals +n_times = 2 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 0.6 # cm r_root = 0.02 # cm @@ -127,7 +127,7 @@ def run_perirhizal_test(): #DS_W = 1.902e-5 #cm2/s #Ds = DS_W / 10000#m2/s - Ds = 1.902e-5 / 2#cm2/s # division by 2: from NO3 to H2PO4 + Ds = 1.902e-5 #cm2/s # division by 2: from NO3 to H2PO4 Ds = Ds * 24 * 3600 #cm2/d outer_waterpotential = initial_waterpotential #cm @@ -217,7 +217,7 @@ def run_perirhizal_test(): for r, dt in enumerate(dt_): - dampening = 0.01 #slow down the inflow from outside + dampening = 0*0.01 #slow down the inflow from outside time = simtimes[r] print('time',time) @@ -275,7 +275,8 @@ def run_perirhizal_test(): soilSoilFluxes = s.getOuterFlow(0, length) * dt # cm3 soilSoilFluxesS = s.getOuterFlow(1, length) * dt # mol - rootwateruptake = abs(rootSoilFluxes) / dt + rootwateruptake = abs(rootSoilFluxes) /length + #rootwateruptake = [abs(waterdemand)] #test TODO: remove this waterinflow = np.array([abs(outer_watersource * volumes[-1])]) @@ -291,6 +292,7 @@ def run_perirhizal_test(): mean_water = total_water / (points[NC-1]**2 - points[0]**2) total_solute = total_solute / (points[NC-1]**2 - points[0]**2) mean_solute = total_solute / mean_water + print("mean solute",mean_solute) #translate the mean water content to a mean matrix potential sx = vg.pressure_head(mean_water, peri.sp) @@ -301,13 +303,18 @@ def run_perirhizal_test(): #compute both matrix flux potentials: Phi_root = np.array(vg.fast_mfp[sp](h_sr)) Phi_soil = np.array(vg.fast_mfp[sp](sx)) + Phi_soil_dumux = Phi_soil #compute the spatial watercontents Phi_A, Phi_C = peri.determine_mfp_function(Phi_root, Phi_soil, rho) #alternative computation using the waterdemand - Phi_A = abs(waterdemand)*r_root*((rho)**2/(2*(1-rho**2))) - Phi_C = Phi_soil - Phi_A + Phi_A = (abs(waterdemand)*r_root)*((rho)**2/(2*(1-rho**2))) #TODO. why *10? + #Phi_C = Phi_soil - Phi_A #inaccurate, as the mean mfp will then only be reached at the outer boundary + Phi_C = Phi_soil - (0.53**2+2*np.log(1/0.53))*Phi_A + + Phi_root_srnf = Phi_A*(1/rho**2+2*np.log(rho))+Phi_C + Phi_outer_srnf = Phi_C #if waterstress, then Phi_A = - rho**2*Phi_C if Phi_A*(1/rho**2-np.log(1/rho**2))+Phi_C <=0: @@ -395,16 +402,16 @@ def run_perirhizal_test(): Phi_out = Phi_A*(1.0**2-np.log(1.0**2))+Phi_C # run the alpha omega model on solute flow (both steady state and steady rate) waterflow = root_wateruptake - result_solutes_ss = peri.soil_root_solutes_ss_([Phi_root], [Phi_test], [mean_solute], [Vmax_per_area], [Km], Ds, [-abs(radial_waterdemand)], peri.sp) - result_solutes_srnf = peri.soil_root_solutes_sr_([Phi_root], [Phi_soil], [rho], [mean_solute], [Vmax_per_area], [Km], Ds, [-abs(radial_waterdemand)], peri.sp) + result_solutes_ss = peri.soil_root_solutes_ss_([Phi_root_srnf], [Phi_outer_srnf], [mean_solute], [Vmax], [Km], Ds, [abs(radial_waterdemand)], peri.sp) + result_solutes_srnf = peri.soil_root_solutes_sr_([Phi_root_srnf], [Phi_outer_srnf], [rho], [mean_solute], [Vmax], [Km], Ds, [abs(radial_waterdemand)], peri.sp) result_solutes_ss = result_solutes_ss[0] result_solutes_srnf = result_solutes_srnf[0] print("steadystate", result_solutes_ss, "steadyrate", result_solutes_srnf) solute_ss[r,1] = result_solutes_ss - solute_ss[r,0] = Vmax_per_area * result_solutes_ss / (Km + result_solutes_ss) + solute_ss[r,0] = Vmax * result_solutes_ss / (Km + result_solutes_ss) solute_srnf[r,1] = result_solutes_srnf - solute_srnf[r,0] = Vmax_per_area * result_solutes_srnf / (Km + result_solutes_srnf) + solute_srnf[r,0] = Vmax * result_solutes_srnf / (Km + result_solutes_srnf) #F0 = peri.lookup_table_solutes((Phi_root,0)) # for the ratio of concentration next to the root to somewhere in the perirhizal zone r_rel = CC[0] / r_prhiz @@ -419,27 +426,27 @@ def run_perirhizal_test(): #F = peri.lookup_table_solutes((Phi_current,0))-F0 F = peri.integral_overDiffusion_(Phi_current,peri.sp)-F0 #print("Ds", Ds, "Dtilde",D_tilde,"F",F,"Dtilde*F",D_tilde*F) - F_tilde=math.exp(-D_tilde*F) - #F_tilde=math.exp(D_tilde*F) + F_tilde=math.exp(D_tilde*F) + F_tilde_inv=math.exp(-D_tilde*F) print("F_tilde", F_tilde) - solute_ss[r,j+1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[r,0] / abs(waterdemand)#waterdemand is assumed to be negative - solute_srnf[r,j+1] = result_solutes_srnf * F_tilde + (1-F_tilde) * solute_srnf[r,0] / abs(waterdemand)#waterdemand is assumed to be negative + solute_ss[r,j+1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[r,0] / abs(radial_waterdemand)#waterdemand is assumed to be negative + solute_srnf[r,j+1] = result_solutes_srnf * F_tilde + (1-F_tilde) * solute_srnf[r,0] / abs(radial_waterdemand) #an uptake of both water and solute is assumed solutes_Tiina[r,0] = peri.solutesuptake_convdiff_([mean_water],[outer_conc], [Vmax], [Km], Ds, [waterflow], [r_root], [0.], [time/1.5], peri.sp)[0] - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ff") - watercontent, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_soil, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_dumux], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ff") + watercontent, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_soil_dumux, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) general_water[r,:] = watercontent[:] general_waterpotential[r,:] = waterpotential[:] general_ff[r,1:] = soluteconcentration[:] - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ss") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_dumux], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ss") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_dumux, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) general_ss[r,1:] = soluteconcentration[:] - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_sr") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_dumux], rootwateruptake, waterinflow, [r_root], [r_prhiz], [mean_solute], [outer_conc], [Vmax], [Km], [Ds], peri.sp, mode = "sr_nf") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_dumux, rootwateruptake[0], waterinflow[0], r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) general_sr[r,1:] = soluteconcentration[:] return water_dumux, solute_dumux, solute_ss, water_sr, solute_srnf, water_sr2, mp_perirhizal1, mp_perirhizal2, solutes_Tiina, general_water, general_waterpotential, general_ss, general_sr, general_ff @@ -494,14 +501,14 @@ def run_perirhizal_test(): # compare both for the differint means of water / solute content run = 0 timestep = np.array([1,3,5,7,9]) -timestep = np.array(np.linspace(1,7,num=5)) #TODO remove +timestep = np.array(np.linspace(1,6,num=5)) #TODO remove print(timestep) for i in range(5): timestep[i] = int(n_times * timestep[i] / 10) timestep = timestep.astype(int) #plot water and nitrogen in that one voxel #fig, ax1 = figure_style.subplots12(1, 5) -fig, ax1 = figure_style.subplots12(nrows=5, ncols=1) +#fig, ax1 = figure_style.subplots12(nrows=5, ncols=1) print(timestep) @@ -515,35 +522,35 @@ def run_perirhizal_test(): #print(water_dumux) #print(CC) #timestep = [100,300,5,7,9] -for i in range(5): - ax2 = ax1[i].twinx() - #print("watercontent dumux", watercontent_dumux) - water_dumux = watercontent_dumux[run, timestep[i], 1:] - water_perirhizal = watercontent_steadyrate[run, timestep[i], 1:] - water_steadyrate2 = watercontent_steadyrate2[run, timestep[i], 1:] - solute_dumux = soluteconcentration_dumux[run, timestep[i], 1:] - solute_steadystate = soluteconcentration_steadystate[run, timestep[i], 1:] - solute_steadyrate = soluteconcentration_steadyrate[run, timestep[i], 1:] - solute_Tiina = np.array([soluteconcentration_Tiina[run, timestep[i], 0] + 1*(outer_conc - soluteconcentration_Tiina[run, timestep[i], 0]) * cc / r_prhiz for cc in CC]) #np.linspace(soluteconcentration_steadyrate[run, timestep[i], 0], outer_conc, NC-1) - - ax1[i].plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") - ax1[i].plot(CC, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") - #ax1[i].plot(CC, water_steadyrate2, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal2") - ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") - #ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") - #ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - ax2.plot(CC, solute_Tiina, "m", linestyle = linestyle_steadystate, label = "solute_Tiina") - - ax1[i].set_xlabel("distance root [cm]") - ax1[i].set_ylabel("water") - ax2.set_ylabel("nitrogen") - ax1[i].legend(["watercontent cm3/cm3"], loc="upper left") - ax2.legend(["nitrogen concentration mol/cm3"], loc="upper right") - - ax1[i].legend(loc="upper left") - ax2.legend(loc="upper right") +# for i in range(5): + # ax2 = ax1[i].twinx() + # #print("watercontent dumux", watercontent_dumux) + # water_dumux = watercontent_dumux[run, timestep[i], 1:] + # water_perirhizal = watercontent_steadyrate[run, timestep[i], 1:] + # water_steadyrate2 = watercontent_steadyrate2[run, timestep[i], 1:] + # solute_dumux = soluteconcentration_dumux[run, timestep[i], 1:] + # solute_steadystate = soluteconcentration_steadystate[run, timestep[i], 1:] + # solute_steadyrate = soluteconcentration_steadyrate[run, timestep[i], 1:] + # solute_Tiina = np.array([soluteconcentration_Tiina[run, timestep[i], 0] + 1*(outer_conc - soluteconcentration_Tiina[run, timestep[i], 0]) * cc / r_prhiz for cc in CC]) #np.linspace(soluteconcentration_steadyrate[run, timestep[i], 0], outer_conc, NC-1) + + # ax1[i].plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") + # ax1[i].plot(CC, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") + # #ax1[i].plot(CC, water_steadyrate2, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal2") + # ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") + # #ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") + # #ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + # ax2.plot(CC, solute_Tiina, "m", linestyle = linestyle_steadystate, label = "solute_Tiina") + + # ax1[i].set_xlabel("distance root [cm]") + # ax1[i].set_ylabel("water") + # ax2.set_ylabel("nitrogen") + # ax1[i].legend(["watercontent cm3/cm3"], loc="upper left") + # ax2.legend(["nitrogen concentration mol/cm3"], loc="upper right") + + # ax1[i].legend(loc="upper left") + # ax2.legend(loc="upper right") #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) -plt.show() +#plt.show() fig, ax1 = figure_style.subplots12(nrows=5, ncols=2) # dumux(both) @@ -579,17 +586,19 @@ def run_perirhizal_test(): water_dumux = vg.pressure_head(water_dumux, peri.sp) water_perirhizal = vg.pressure_head(water_perirhizal, peri.sp) #water_perirhizal2 = vg.pressure_head(water_steadyrate2, peri.sp) + + solute_Tiina = np.array([soluteconcentration_Tiina[run, timestep[i], 0] + 1*(outer_conc - soluteconcentration_Tiina[run, timestep[i], 0]) * cc / r_prhiz for cc in CC]) #np.linspace(soluteconcentration_steadyrate[run, timestep[i], 0], outer_conc, NC-1) ax1[i,0].plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") #ax1[i].plot(CC, water_perirhizal, "b", linestyle = linestyle_steadyrate, label = "water_perirhizal") #this and the following line give the same results, as they should ax1[i,0].plot(CC, mp_steadyrate1, "b", linestyle = linestyle_steadyrate, label = "water_sr") #ax1[i].plot(CC, mp_steadyrate2, "b", linestyle = linestyle_steadystate, label = "water_sr_stress") ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") - #ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") + ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") + #ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") #ax2.plot(CC, solute_steadyrate * solute_dumux[-1] / solute_steadyrate[-1], "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - #ax2.plot(CC, solute_steadystate * solute_dumux[10] / solute_steadystate[10], "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") + #ax2.plot(CC, solute_steadystate * solute_dumux[26] / solute_steadystate[26], "m", linestyle = linestyle_steadystate, label = "solute_steadystate") #ax2.plot(CC, solute_Tiina, "m", linestyle = linestyle_steadystate, label = "solute_Tiina") ax2.plot(CC, solute_Tiina * solute_dumux[-1] / outer_conc, "m", linestyle = linestyle_special, label = "solute_Tiina_scaled") @@ -603,6 +612,7 @@ def run_perirhizal_test(): ax2.legend(loc="upper right") #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) #plt.show() + for i in range(5): ax2 = ax1[i,1].twinx() @@ -614,7 +624,7 @@ def run_perirhizal_test(): solute_steadyrate = general_sr_plot[run, timestep[i], 1:] solute_farfield = general_ff_plot[run, timestep[i], 1:] - #mp_steadyrate1 = matrixpotential_perirhizal1[run, timestep[i], 1:] + mp_steadyrate1 = matrixpotential_perirhizal1[run, timestep[i], 1:] #mp_steadyrate2 = matrixpotential_perirhizal2[run, timestep[i], 1:] @@ -623,14 +633,14 @@ def run_perirhizal_test(): #water_perirhizal2 = vg.pressure_head(water_steadyrate2, peri.sp) ax1[i,1].plot(CC, water_dumux, "b", linestyle = linestyle_dumux, label = "water_dumux") - ax1[i,1].plot(CC, water_general, "b", linestyle = linestyle_steadyrate, label = "water_general") #this and the following line give the same results, as they should + #ax1[i,1].plot(CC, water_general, "b", linestyle = linestyle_special, label = "water_general") #this and the following line give the same results, as they should ax1[i,1].plot(CC, mp_steadyrate1, "b", linestyle = linestyle_steadyrate, label = "water_sr") #ax1[i].plot(CC, mp_steadyrate2, "b", linestyle = linestyle_steadystate, label = "water_sr_stress") ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") #ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") - ax2.plot(CC, solute_steadyrate, "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadyrate, label = "solute_steadystate") - ax2.plot(CC, solute_farfield, "m", linestyle = linestyle_special, label = "solute_farfield") + #ax2.plot(CC, solute_steadyrate * solute_dumux[-1] / solute_steadyrate[-1], "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") + ax2.plot(CC, solute_steadystate * solute_dumux[-1] / solute_steadystate[-1], "m", linestyle = linestyle_steadystate, label = "solute_steadystate") + ax2.plot(CC, solute_farfield * solute_dumux[-1] / solute_farfield[-1], "m", linestyle = linestyle_special, label = "solute_farfield") #ax2.plot(CC, solute_Tiina * solute_dumux[-1] / outer_conc, "m", linestyle = linestyle_special, label = "solute_Tiina_scaled") ax1[i,1].set_xlabel("distance root [cm]") From 5ccffe8cb83ebe475dcfd787db047b5e5ec374a4 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Fri, 19 Jun 2026 15:33:41 +0200 Subject: [PATCH 24/41] ss and sr seem ready --- src/functional/Perirhizal.py | 4 ++-- test/test_perirhizal.py | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 5b95c60a5..0662271b6 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -617,7 +617,7 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf a2=(1-F_tilde_inv[i])/(waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 - rsc[i]=-p/2-math.sqrt(pow(p/2,2)-q) + rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) return rsc @@ -685,7 +685,7 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w if q>0: print("q>0",Km[i],a1,F_tilde_inv[i],R_sr[i],c_bulk[i],watercontent[i]) q=0 - rsc[i]=-p/2-math.sqrt(pow(p/2,2)-q) + rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) return rsc diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 57809d35f..e366c91c2 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -150,7 +150,7 @@ def run_perirhizal_test(): #test the plots Phi_soil = vg.fast_mfp[peri.sp](-100) r_root = 0.02 - r_prhiz = 1 + r_prhiz = 0.6 r_eval = CC#np.linspace(r_root, r_prhiz, 100) rootwateruptake = 0.2 waterinflow = 0.1 @@ -314,7 +314,7 @@ def run_perirhizal_test(): Phi_C = Phi_soil - (0.53**2+2*np.log(1/0.53))*Phi_A Phi_root_srnf = Phi_A*(1/rho**2+2*np.log(rho))+Phi_C - Phi_outer_srnf = Phi_C + Phi_outer_srnf = Phi_A + Phi_C #if waterstress, then Phi_A = - rho**2*Phi_C if Phi_A*(1/rho**2-np.log(1/rho**2))+Phi_C <=0: @@ -402,8 +402,8 @@ def run_perirhizal_test(): Phi_out = Phi_A*(1.0**2-np.log(1.0**2))+Phi_C # run the alpha omega model on solute flow (both steady state and steady rate) waterflow = root_wateruptake - result_solutes_ss = peri.soil_root_solutes_ss_([Phi_root_srnf], [Phi_outer_srnf], [mean_solute], [Vmax], [Km], Ds, [abs(radial_waterdemand)], peri.sp) - result_solutes_srnf = peri.soil_root_solutes_sr_([Phi_root_srnf], [Phi_outer_srnf], [rho], [mean_solute], [Vmax], [Km], Ds, [abs(radial_waterdemand)], peri.sp) + result_solutes_ss = peri.soil_root_solutes_ss_([Phi_root_srnf], [Phi_outer_srnf], [mean_solute], [Vmax], [Km], Ds, [-abs(radial_waterdemand)], peri.sp) + result_solutes_srnf = peri.soil_root_solutes_sr_([Phi_root_srnf], [Phi_outer_srnf], [rho], [mean_solute], [Vmax], [Km], Ds, [-abs(radial_waterdemand)], peri.sp) result_solutes_ss = result_solutes_ss[0] result_solutes_srnf = result_solutes_srnf[0] print("steadystate", result_solutes_ss, "steadyrate", result_solutes_srnf) @@ -421,7 +421,13 @@ def run_perirhizal_test(): for j in range(NC-1): r_rel = CC[j] / r_prhiz + #Phi_root_srnf = Phi_A*(1/rho**2+2*np.log(rho))+Phi_C + #Phi_outer_srnf = Phi_A + Phi_C + det = (1/rho**2+2*np.log(rho)) - 1 + Phi_A = (1 * Phi_root_srnf - 1 * Phi_outer_srnf)/det + Phi_C = ( -1 * Phi_root_srnf + (1/rho**2+2*np.log(rho)) * Phi_outer_srnf)/det Phi_current = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C + print("Phi_current",Phi_current,Phi_outer_srnf) #Phi_current = waterdemand*r_root*((r_rel*rho)**2/(2*(1-rho**2))-rho**2/(1-rho**2)*(np.log(r_rel**2)+0.5))+Phi_soil #F = peri.lookup_table_solutes((Phi_current,0))-F0 F = peri.integral_overDiffusion_(Phi_current,peri.sp)-F0 @@ -429,8 +435,8 @@ def run_perirhizal_test(): F_tilde=math.exp(D_tilde*F) F_tilde_inv=math.exp(-D_tilde*F) print("F_tilde", F_tilde) - solute_ss[r,j+1] = result_solutes_ss * F_tilde + (1-F_tilde) * solute_ss[r,0] / abs(radial_waterdemand)#waterdemand is assumed to be negative - solute_srnf[r,j+1] = result_solutes_srnf * F_tilde + (1-F_tilde) * solute_srnf[r,0] / abs(radial_waterdemand) #an uptake of both water and solute is assumed + solute_ss[r,j+1] = result_solutes_ss * F_tilde - (1-F_tilde) * solute_ss[r,0] / abs(radial_waterdemand)#waterdemand is assumed to be negative + solute_srnf[r,j+1] = result_solutes_srnf * F_tilde - (1-F_tilde) * solute_srnf[r,0] / abs(radial_waterdemand) #an uptake of both water and solute is assumed solutes_Tiina[r,0] = peri.solutesuptake_convdiff_([mean_water],[outer_conc], [Vmax], [Km], Ds, [waterflow], [r_root], [0.], [time/1.5], peri.sp)[0] From 0e515e5c7e37ede087685fe368682b82404601f8 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Fri, 19 Jun 2026 16:35:36 +0200 Subject: [PATCH 25/41] general soluteflow produces reasonable results --- src/functional/Perirhizal.py | 32 ++++++++++++++++---------------- test/test_perirhizal.py | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 0662271b6..7374378da 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -314,19 +314,19 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, Ds0 = Ds[i] wateruptake_rel = (rootwateruptake[i] - waterinflow[i]) / ((r_prhiz[i]**2 - r_root[i]**2) * np.pi) critical_wateruptake = rootwateruptake[i] + wateruptake_rel * ((r_root[i]**2 - r_crit**2) * np.pi) - r_eval = [r_crit, r_root[i], r_prhiz[i]] + r_eval = [r_crit, r_root[i], (r_root[i]+r_prhiz[i])/2, r_prhiz[i]] #use the subfunction #TODO: alternative lookup table conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) #fix discrepancy between critical and root radius #TODO: turn the means into floats instead of lists with one entry - conc_rel_c = conc_rel_c[1:]/conc_rel_c[1] - conc_mean_c = (conc_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-conc_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) - inflow_rel_c = inflow_rel_c[1:]/inflow_rel_c[1] - inflow_mean_c = (inflow_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-inflow_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) - Uptake_rel_c = Uptake_rel_c[1:]/Uptake_rel_c[1] - Uptake_mean_c = (Uptake_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-Uptake_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) + #conc_rel_c = conc_rel_c[1:]#/conc_rel_c[1] + #conc_mean_c = (conc_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-conc_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) + #inflow_rel_c = inflow_rel_c[1:]#/inflow_rel_c[1] + #inflow_mean_c = (inflow_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-inflow_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) + #Uptake_rel_c = Uptake_rel_c[1:]#/Uptake_rel_c[1] + #Uptake_mean_c = (Uptake_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-Uptake_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) #default prefactors for the steady state case pre_c = 0 @@ -373,16 +373,16 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, b = np.zeros((4,2)) #right hand side of the linear equation, one for absolute values, one for the rsc value #c11 * ss flow + c12 * sr uptake + c13 * rsc = c_mean - A[0,0] = inflow_mean_c[0] - A[0,1] = Uptake_mean_c[0] - b[0,1] = -conc_mean_c[0] + A[0,0] = inflow_mean_c[-1] + A[0,1] = Uptake_mean_c[-1] + b[0,1] = -conc_mean_c[-1] b[0,0] = c_soil[i] #c21 * ss flow + c22 * sr uptake + c23 * rsc = c_prhiz - A[1,0] = inflow_rel_c[0] - A[1,1] = Uptake_rel_c[0] + A[1,0] = inflow_rel_c[-1] + A[1,1] = Uptake_rel_c[-1] A[1,2] = -1 - b[1,1] = -conc_rel_c[0] + b[1,1] = -conc_rel_c[-1] #ss flow + sr uptake = Uptake A[2,0] = 1 @@ -481,7 +481,7 @@ def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_pr conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) - soluteconcentration = c_root * conc_rel_c[1:] + ss_uptake * inflow_rel_c[1:] + sr_uptake * Uptake_rel_c[1:] + soluteconcentration = c_root * conc_rel_c + ss_uptake * inflow_rel_c + sr_uptake * Uptake_rel_c watercontent = np.array([watercontent_func(r) for r in r_eval]) waterpotential = np.array([waterpotential_func(r) for r in r_eval]) @@ -513,7 +513,7 @@ def solute_linearequation_sr_(self, Ds0, critical_wateruptake, wateruptake_rel, #TODO:formulate this for ln r so as to make it more robust against small critical radii r_crit = r_eval[0] - + r_eval = r_eval[1:] n = len(r_eval) conc_rel_c = np.ones(n) @@ -550,7 +550,7 @@ def solute_linearequation_sr_(self, Ds0, critical_wateruptake, wateruptake_rel, conc_mean_c = np.array([np.average(conc_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) inflow_mean_c = np.array([np.average(inflow_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) Uptake_mean_c = np.array([np.average(Uptake_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) - + print("n conc_mean_c",n,len(conc_mean_c), conc_mean_c) return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c def soil_root_solutes_combined(self, Phi_soil, rootwateruptake, soilwateruptake, c_prhiz, c_bulk, Vmax, Km, Ds, waterflow, sp): diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index e366c91c2..66e1b19b9 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -645,8 +645,8 @@ def run_perirhizal_test(): ax2.plot(CC, solute_dumux, "m", linestyle = linestyle_dumux, label = "solute_dumux") #ax2.plot(CC, solute_steadystate, "m", linestyle = linestyle_steadystate, label = "solute_steadystate") #ax2.plot(CC, solute_steadyrate * solute_dumux[-1] / solute_steadyrate[-1], "m", linestyle = linestyle_steadyrate, label = "solute_steadyrate") - ax2.plot(CC, solute_steadystate * solute_dumux[-1] / solute_steadystate[-1], "m", linestyle = linestyle_steadystate, label = "solute_steadystate") - ax2.plot(CC, solute_farfield * solute_dumux[-1] / solute_farfield[-1], "m", linestyle = linestyle_special, label = "solute_farfield") + ax2.plot(CC, solute_steadystate , "m", linestyle = linestyle_steadystate, label = "solute_steadystate") + ax2.plot(CC, solute_farfield , "m", linestyle = linestyle_special, label = "solute_farfield") #ax2.plot(CC, solute_Tiina * solute_dumux[-1] / outer_conc, "m", linestyle = linestyle_special, label = "solute_Tiina_scaled") ax1[i,1].set_xlabel("distance root [cm]") From a644fb3d83964a413e213b078ec826e37729bbe4 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sat, 20 Jun 2026 10:59:23 +0200 Subject: [PATCH 26/41] satisfactory test for general ss solute uptake --- test/test_perirhizal.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_perirhizal.py b/test/test_perirhizal.py index 66e1b19b9..0714d2084 100644 --- a/test/test_perirhizal.py +++ b/test/test_perirhizal.py @@ -33,8 +33,8 @@ # general parameters -max_time = 1 #d -n_times = 2 # number of time intervals +max_time = 10 #d +n_times = 2000 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 0.6 # cm r_root = 0.02 # cm @@ -217,7 +217,7 @@ def run_perirhizal_test(): for r, dt in enumerate(dt_): - dampening = 0*0.01 #slow down the inflow from outside + dampening = 0.01 #slow down the inflow from outside time = simtimes[r] print('time',time) @@ -262,8 +262,8 @@ def run_perirhizal_test(): #s.setParameter( "Soil.BC.Bot.SValue", str(root_soluteuptake)) #s.initializeProblem(maxDt = 0.01) - s.setSource({NC-2: 1 * outer_watersource * volumes[-1] *length}) - s.setSource({0: root_soluteuptake * 62, NC-2: 1* outer_solutesource * volumes[-1] * length * 62}, eq_idx = 1) #factor 1000 is important for dumux as in the richards wrapper it is divided by 1000 + s.setSource({NC-2: 100 * outer_watersource * volumes[-1] *length}) + s.setSource({0: 10*root_soluteuptake * 62, NC-2: 1* outer_solutesource * volumes[-1] * length * 62}, eq_idx = 1) #factor 1000 is important for dumux as in the richards wrapper it is divided by 1000 s.solve(dt, saveInnerFluxes_ = True) Wvolafter = cellVolumes*s.getWaterContent() # cm3 From cf04e2901ed8f52ddcdbae241997e1a826e70b0a Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Wed, 24 Jun 2026 07:57:06 +0200 Subject: [PATCH 27/41] moving the test file to the location of test files --- .../test_perirhizal.py | 120 ++++++++++++++---- 1 file changed, 96 insertions(+), 24 deletions(-) rename {test => experimental/analytical_model_perirhizal}/test_perirhizal.py (82%) diff --git a/test/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py similarity index 82% rename from test/test_perirhizal.py rename to experimental/analytical_model_perirhizal/test_perirhizal.py index 0714d2084..de204b17b 100644 --- a/test/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -34,7 +34,7 @@ # general parameters max_time = 10 #d -n_times = 2000 # number of time intervals +n_times = 1000 # number of time intervals times = np.linspace(0,max_time,n_times)[1:] r_prhiz = 0.6 # cm r_root = 0.02 # cm @@ -51,6 +51,8 @@ #space for the oupputs watercontent_dumux = np.zeros((n_tests, n_times, n_sp+1)) soluteconcentration_dumux = np.zeros((n_tests, n_times, n_sp+1)) +watercontent_dumux2 = np.zeros((n_tests, n_times, n_sp+1)) +soluteconcentration_dumux2 = np.zeros((n_tests, n_times, n_sp+1)) soluteconcentration_steadystate = np.zeros((n_tests, n_times, n_sp+1)) watercontent_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) watercontent_steadyrate2 = np.zeros((n_tests, n_times, n_sp+1)) @@ -88,8 +90,10 @@ def run_perirhizal_test(): #space for the outputs water_dumux = np.zeros((n_times, n_sp+1)) + water_dumux2 = np.zeros((n_times, n_sp+1)) water_sr2 = np.zeros((n_times, n_sp+1)) solute_dumux = np.zeros((n_times, n_sp+1)) + solute_dumux2 = np.zeros((n_times, n_sp+1)) water_ss = np.zeros((n_times, n_sp+1)) solute_ss = np.zeros((n_times, n_sp+1)) water_sr = np.zeros((n_times, n_sp+1)) @@ -119,7 +123,7 @@ def run_perirhizal_test(): # root conductivity and solute uptake parameters, it is chosen to be constant throughout the entire simulation time root_conductivity = 1e-4 #1/d inner_kr = root_conductivity * r_root * 2 * 3.14 - waterdemand = -0.1 #cm/d + waterdemand = -0.05 #cm/d radial_waterdemand = 2*3.14*r_root * waterdemand #cm2/d Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) @@ -127,7 +131,7 @@ def run_perirhizal_test(): #DS_W = 1.902e-5 #cm2/s #Ds = DS_W / 10000#m2/s - Ds = 1.902e-5 #cm2/s # division by 2: from NO3 to H2PO4 + Ds = 1.902e-5 / 2 #cm2/s # division by 2: from NO3 to H2PO4 Ds = Ds * 24 * 3600 #cm2/d outer_waterpotential = initial_waterpotential #cm @@ -153,7 +157,7 @@ def run_perirhizal_test(): r_prhiz = 0.6 r_eval = CC#np.linspace(r_root, r_prhiz, 100) rootwateruptake = 0.2 - waterinflow = 0.1 + waterinflow = 0.05 c_root = 0* 1e-7 Ds0 = Ds ss_uptake = 0*1.e-7 @@ -181,6 +185,7 @@ def run_perirhizal_test(): # initialise the dumux model s = RichardsWrapper(RichardsNCCylFoam()) + s2 = RichardsWrapper(RichardsNCCylFoam()) s.initialize() s.createGrid1d(np.linspace(r_root, r_prhiz, NC), length = length/100) # [m] -> [cm] @@ -203,8 +208,32 @@ def run_perirhizal_test(): #s.setParameter("Component.MolarMass", "1.8e-2") s.setParameter("Component.MolarMass", "62.0") - s.setParameter("Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600))) # m2 s-1 + s.setParameter("Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600) / 1000)) # m2 s-1 #TODO: this is not doing anything s.initializeProblem(maxDt = 0.01) + + s2.initialize() + s2.createGrid1d(np.linspace(r_root, r_prhiz, NC), length = length/100) # [m] -> [cm] + s2.setVGParameters([soilVG]) + + # theta = 0.378, benchmark is set be nearly fully saturated, so we don't care too much about the specific values + s2.setHomogeneousIC(initial_waterpotential) # cm pressure head + + s2.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")# + #s.setTopBC("constantPressure",-200) # [cm/day] "noFlux")# + #s.setBotBC("constantFluxCyl",0.0) # "noFlux")# + s2.setBotBC("constantFluxCyl",waterdemand) # "noFlux")# Flux in cm/d + s2.setParameter("Soil.BC.Top.SType", "3") # michaelisMenten=8 (SType = Solute Type) + s2.setParameter("Soil.BC.Top.CValue", "0.0") # michaelisMenten=8 (SType = Solute Type) + s2.setParameter("Soil.BC.Bot.SType", "3") # michaelisMenten=8 (SType = Solute Type) + s2.setParameter("Soil.BC.Bot.CValue", "0.0") + + s2.setParameter("Soil.IC.C", str(initial_soluteconcentration)) # g / cm3 # TODO specialised setter? + + #s.setParameter("Component.MolarMass", "1.8e-2") + s2.setParameter("Component.MolarMass", "62.0") + + s2.setParameter("Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600))) # m2 s-1 + s2.initializeProblem(maxDt = 0.01) cellVolumes = s.getCellSurfacesCyl() * length # cm3 print("cellVolumes",cellVolumes) @@ -235,6 +264,8 @@ def run_perirhizal_test(): current_potential = s.getSolutionHead() water_dumux[r,1:] = vg.water_content(current_potential,peri.sp) + current_potential2 = s2.getSolutionHead() + water_dumux2[r,1:] = vg.water_content(current_potential2,peri.sp) current_rs_potential = current_potential[0] current_outer_potential = current_potential[-1] current_outer_watercontent = vg.water_content(current_outer_potential, peri.sp) @@ -247,15 +278,20 @@ def run_perirhizal_test(): #alternative: adapt root matrix potential for a steady uptake: rx = max(waterdemand / root_conductivity + current_rs_potential, -14750) #note: waterdemand is assumed to be negative + rx2 = max(waterdemand / root_conductivity + current_potential2[0], -14750) #rx = current_rs_potential #model root solute uptake #note: solutes are given in g current_concentration = s.getSolution(1) + current_concentration2 = s2.getSolution(1) solute_dumux[r,1:] = current_concentration + solute_dumux2[r,1:] = current_concentration current_rs_concentration = current_concentration[0] current_outer_concentration = current_concentration[-1] root_soluteuptake = - Vmax * max(current_rs_concentration,0) / (Km + current_rs_concentration) # mol / d + root_soluteuptake2 = - Vmax * max(current_concentration2[0],0) / (Km + max(current_concentration2[0],0)) # mol / d + outer_solutesource = ( outer_conc - current_outer_concentration ) * current_outer_watercontent / dt * dampening solute_dumux[r,0] = root_soluteuptake print(solute_dumux) @@ -263,12 +299,17 @@ def run_perirhizal_test(): #s.initializeProblem(maxDt = 0.01) s.setSource({NC-2: 100 * outer_watersource * volumes[-1] *length}) - s.setSource({0: 10*root_soluteuptake * 62, NC-2: 1* outer_solutesource * volumes[-1] * length * 62}, eq_idx = 1) #factor 1000 is important for dumux as in the richards wrapper it is divided by 1000 + s.setSource({0: root_soluteuptake*62*10, NC-2: outer_solutesource * volumes[-1] * length * 62}, eq_idx = 1) #factor 1000 is important for dumux as in the richards wrapper it is divided by 1000 + s2.setSource({0: root_soluteuptake2*1000}) s.solve(dt, saveInnerFluxes_ = True) + s2.solve(dt, saveInnerFluxes_ = True) Wvolafter = cellVolumes*s.getWaterContent() # cm3 Smassafter = s.getSolution(1) * Wvolafter # mol + Wvolafter2 = cellVolumes*s2.getWaterContent() # cm3 + Smassafter2 = s.getSolution(1) * Wvolafter2 # mol + rootSoilFluxes = s.getInnerFlow(0, length) * dt # cm3 rootSoilFluxesS = s.getInnerFlow(1, length) * dt # mol @@ -277,7 +318,7 @@ def run_perirhizal_test(): rootwateruptake = abs(rootSoilFluxes) /length #rootwateruptake = [abs(waterdemand)] #test TODO: remove this - waterinflow = np.array([abs(outer_watersource * volumes[-1])]) + waterinflow = np.array([abs(100*outer_watersource * volumes[-1])]) @@ -286,35 +327,52 @@ def run_perirhizal_test(): # mean water content of the rhizosphere total_water = 0 total_solute = 0 + total_water2 = 0 + total_solute2 = 0 for j in range(NC-1): total_water = total_water + water_dumux[r,j+1]*(points[j+1]**2 - points[j]**2) total_solute = total_solute + water_dumux[r,j+1]*solute_dumux[r,j+1]*(points[j+1]**2 - points[j]**2) + total_water2 = total_water2 + water_dumux2[r,j+1]*(points[j+1]**2 - points[j]**2) + total_solute2 = total_solute2 + water_dumux2[r,j+1]*solute_dumux2[r,j+1]*(points[j+1]**2 - points[j]**2) mean_water = total_water / (points[NC-1]**2 - points[0]**2) total_solute = total_solute / (points[NC-1]**2 - points[0]**2) mean_solute = total_solute / mean_water + mean_water2 = total_water2 / (points[NC-1]**2 - points[0]**2) + total_solute2 = total_solute2 / (points[NC-1]**2 - points[0]**2) + mean_solute2 = total_solute2 / mean_water print("mean solute",mean_solute) #translate the mean water content to a mean matrix potential sx = vg.pressure_head(mean_water, peri.sp) + sx2 = vg.pressure_head(mean_water2, peri.sp) #start the steady rate solver h_sr = peri.soil_root_interface_(rx, sx, inner_kr, rho, peri.sp) + h_sr2 = peri.soil_root_interface_(rx2, sx2, inner_kr, rho, peri.sp) #compute both matrix flux potentials: Phi_root = np.array(vg.fast_mfp[sp](h_sr)) Phi_soil = np.array(vg.fast_mfp[sp](sx)) Phi_soil_dumux = Phi_soil + Phi_root2 = np.array(vg.fast_mfp[sp](h_sr2)) + Phi_soil2 = np.array(vg.fast_mfp[sp](sx2)) + #compute the spatial watercontents Phi_A, Phi_C = peri.determine_mfp_function(Phi_root, Phi_soil, rho) + Phi_A2, Phi_C2 = peri.determine_mfp_function(Phi_root2, Phi_soil2, rho) #alternative computation using the waterdemand Phi_A = (abs(waterdemand)*r_root)*((rho)**2/(2*(1-rho**2))) #TODO. why *10? + Phi_A2 = (abs(waterdemand)*r_root)*((rho)**2/(2*(1-rho**2))) #Phi_C = Phi_soil - Phi_A #inaccurate, as the mean mfp will then only be reached at the outer boundary Phi_C = Phi_soil - (0.53**2+2*np.log(1/0.53))*Phi_A + Phi_C2 = Phi_soil2 - (0.53**2+2*np.log(1/0.53))*Phi_A2 Phi_root_srnf = Phi_A*(1/rho**2+2*np.log(rho))+Phi_C Phi_outer_srnf = Phi_A + Phi_C + Phi_root_srnf2 = Phi_A2*(1/rho**2+2*np.log(rho))+Phi_C2 + Phi_outer_srnf2 = Phi_A2 + Phi_C2 #if waterstress, then Phi_A = - rho**2*Phi_C if Phi_A*(1/rho**2-np.log(1/rho**2))+Phi_C <=0: @@ -348,11 +406,17 @@ def run_perirhizal_test(): print(MFP_out) #MFP_out = MFP_out /10000 - + r_rel = CC[0] / r_prhiz + Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C + mp_perirhizal1[r,0]=vg.fast_imfp[sp](Phi) + Phi2 = Phi_A2*(r_rel**2-np.log(r_rel**2))+Phi_C2 + mp_perirhizal2[r,0]=vg.fast_imfp[sp](Phi2) for j in range(NC-1): r_rel = CC[j] / r_prhiz Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C mp_perirhizal1[r,j+1]=vg.fast_imfp[sp](Phi) # TODO: adapt this to stress maybe? + Phi2 = Phi_A2*(r_rel**2-np.log(r_rel**2))+Phi_C2 + mp_perirhizal2[r,j+1]=vg.fast_imfp[sp](Phi2) # TODO: adapt this to stress maybe? #Phi_soil = Phi_out # TODO: remove this #if water_dumux[r,j+1] Date: Wed, 24 Jun 2026 14:32:05 +0200 Subject: [PATCH 28/41] rewrote the test skript for soluteflow --- .../test_perirhizal.py | 1025 ++++++----------- 1 file changed, 361 insertions(+), 664 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index de204b17b..bf2b62b94 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -1,14 +1,23 @@ -# this is a test file by Erik Kopp to test the alternative implementation of the perirhizal resistances and the perirhizal diffusion +# this is a test file by Erik Kopp to test the alternative implementation of the perirhizal resistances (water) and solute flow +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import time -import sys; sys.path.append("../src/functional") -sys.path.append("../../dumux-rosi/build-cmake/cpp/python_binding/") -sys.path.append("../../../dumuxtest/dumux/dumux-rosi/build-cmake/cpp/python_binding/") +import sys; sys.path.append("../.."); sys.path.append("../../src/") +import plantbox as pb #from plantbox import Perirhizal from plantbox.functional.Perirhizal import PerirhizalPython -from numpy import linalg as LA import plantbox.functional.van_genuchten as vg +from plantbox.visualisation import figure_style +#import Perirhizal + + +# dumux rosi imports +#sys.path.append("../../dumux-rosi/build-cmake/cpp/python_binding/") +#sys.path.append("../../../dumuxtest/dumux/dumux-rosi/build-cmake/cpp/python_binding/") from rosi.richards_flat import RichardsFlatWrapper as RichardsWrapper # Python part, macroscopic soil model #from richards import RichardsWrapper # Python part from rosi.richards_no_mpi import RichardsNoMPIWrapper # Python part of cylindrcial @@ -16,111 +25,94 @@ #from rosi_richardsnc_cyl import RichardsNCCylFoam as RichardsNC_cyl # C++ part (Dumux binding) #from rosi_richards22c import RichardsNCSPILU as RichardsNCSP #test -import Perirhizal -import pandas as pd -import numpy as np -import time -import math -import matplotlib.pyplot as plt -from plantbox.visualisation import figure_style + +#numerics +import math from scipy.optimize import fsolve, root_scalar +from numpy import linalg as LA -# run the dumux implementation of root water and nitrate uptake an then compare it to the alpha omega model +# run the dumux implementation of root water and nitrate uptake, later compare it to the analytical approximation n_tests = 1 #try everything here for this many random parameter sets do_computation = True #should the computation be run or take the data from a saved file # general parameters - max_time = 10 #d n_times = 1000 # number of time intervals -times = np.linspace(0,max_time,n_times)[1:] -r_prhiz = 0.6 # cm -r_root = 0.02 # cm +r_prhiz = 0.6 # perirhizal radius[cm] +r_root = 0.02 # root radius [cm] +NC = 40 # number of spatial discretisations +length = 1 #default length of the segment, will not change the outcpme as all variables are constant in this direction [cm] +#two scenarios will be computed: one without inflow, another with a Cauchy BC +n_scenarios = 2 -rho = r_prhiz / r_root -NC = 41 # number of spatial discretisations -n_sp = NC - 1 +#initial conditions +initial_waterpotential = -100 +initial_soluteconcentration = 2e-5#mol/cm3 -length = 1 #cm? +#space for the outputs +# number of tests, number of scenarios, number of timesteps, (rootuptake, inflow, discretisation) +#water outputs +#simulations in dumux +watercontent_dumux = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #watercontent in cm3/cm3, (radial) uptake and inflow in cm2/d +waterpotential_dumux = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #waterpotential in cm, (radial) uptake and inflow in cm2/d +#analytical approximations +watercontent_sr = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #watercontent in cm3/cm3, (radial) uptake and inflow in cm2/d +waterpotential_sr = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #waterpotential in cm, (radial) uptake and inflow in cm2/d + +#solute outputs +#simulations in dumux +solutes_dumux = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm d) +#analytical approximations +#steady state approximations +solutes_dumux_ss = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm d) +#steady rate approximations +solutes_dumux_sr = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm d) +#general steady rate with far field approximation +solutes_dumux_ff = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm d) -dt = max_time / n_times -#space for the oupputs -watercontent_dumux = np.zeros((n_tests, n_times, n_sp+1)) -soluteconcentration_dumux = np.zeros((n_tests, n_times, n_sp+1)) -watercontent_dumux2 = np.zeros((n_tests, n_times, n_sp+1)) -soluteconcentration_dumux2 = np.zeros((n_tests, n_times, n_sp+1)) -soluteconcentration_steadystate = np.zeros((n_tests, n_times, n_sp+1)) -watercontent_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) -watercontent_steadyrate2 = np.zeros((n_tests, n_times, n_sp+1)) -soluteconcentration_steadyrate = np.zeros((n_tests, n_times, n_sp+1)) -matrixpotential_perirhizal1 = np.zeros((n_tests, n_times, n_sp+1)) -matrixpotential_perirhizal2 = np.zeros((n_tests, n_times, n_sp+1)) -general_matrixpotential = np.zeros((n_tests, n_times, n_sp+1)) -general_water = np.zeros((n_tests, n_times, n_sp+1)) -general_ss = np.zeros((n_tests, n_times, n_sp+1)) -general_sr = np.zeros((n_tests, n_times, n_sp+1)) -general_ff = np.zeros((n_tests, n_times, n_sp+1)) -soluteconcentration_Tiina = np.zeros((n_tests, n_times, n_sp+1)) -soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil -soilVG = [0.08, 0.43, 0.04, 1.6, 50] #loam from benchmark #da war am Ende noch eine 0.5, keine Ahnung warum, vermutlich eine vorherige Programmversion -lb = 0.5 -points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), - NC, base = lb) -CC = np.array([(points[i] + points[i+1])/2 for i in range(len(points)-1)]) -volumes = np.array([(points[i+1]**2 - points[i]**2)*3.14 for i in range(len(points)-1)]) -initial_waterpotential = -300 -initial_soluteconcentration = 2e-5#mol/cm3 outer_conc = initial_soluteconcentration -def run_perirhizal_test(): - - global r_root, r_prhiz +def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenarios, initial_waterpotential, initial_soluteconcentration): #space for the outputs - water_dumux = np.zeros((n_times, n_sp+1)) - water_dumux2 = np.zeros((n_times, n_sp+1)) - water_sr2 = np.zeros((n_times, n_sp+1)) - solute_dumux = np.zeros((n_times, n_sp+1)) - solute_dumux2 = np.zeros((n_times, n_sp+1)) - water_ss = np.zeros((n_times, n_sp+1)) - solute_ss = np.zeros((n_times, n_sp+1)) - water_sr = np.zeros((n_times, n_sp+1)) - water_test = np.zeros((n_times, n_sp+1)) - solute_srnf = np.zeros((n_times, n_sp+1)) - - general_water = np.zeros((n_times, n_sp+1)) - general_waterpotential = np.zeros((n_times, n_sp+1)) - - general_ss = np.zeros((n_times, n_sp+1)) - general_sr = np.zeros((n_times, n_sp+1)) - general_ff = np.zeros((n_times, n_sp+1)) - - mp_perirhizal1 = np.zeros((n_times, n_sp+1)) - mp_perirhizal2 = np.zeros((n_times, n_sp+1)) - - mean_water = np.zeros((n_times)) - mean_solutes = np.zeros((n_times)) - - solutes_Tiina = np.zeros((n_times, n_sp+1)) - - # determine some random parameters - #initial_waterpotential = -700 #+ np.random.rand() * 50 #cm3/cm3 #or choose an initial pressure head? - #initial_soluteconcentration = 2e-5#*(1.0+np.random.rand()) #g/cm3 #TODO: lookup realistic concnetration, maybe 10 times the Michaelis Menten half saturation? - print("initial_soluteconcentration",initial_soluteconcentration) - - # root conductivity and solute uptake parameters, it is chosen to be constant throughout the entire simulation time + watercontent_dumux = np.zeros((n_scenarios, n_times, NC+2)) #watercontent in cm3/cm3, (radial) uptake and inflow in cm/d + waterpotential_dumux = np.zeros((n_scenarios, n_times, NC+2)) #waterpotential in cm, (radial) uptake and inflow in cm/d + watercontent_sr = np.zeros((n_scenarios, n_times, NC+2)) #watercontent in cm3/cm3, (radial) uptake and inflow in cm/d + waterpotential_sr = np.zeros((n_scenarios, n_times, NC+2)) #waterpotential in cm, (radial) uptake and inflow in cm/d + solutes_dumux = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) + solutes_dumux_sr = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) + solutes_dumux_ss = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) + solutes_dumux_ff = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) + + simtimes = np.linspace(0,max_time,n_times)[1:] + dt = max_time / n_times + rho = r_prhiz / r_root + + #soil parameters + soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil + + #discretisation + lb = 0.5 + points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), NC+1, base = lb) + CC = np.array([(points[i] + points[i+1])/2 for i in range(NC)]) + volumes = np.array([(points[i+1]**2 - points[i]**2)*3.14 for i in range(NC)]) + + + # root conductivity and solute uptake parameters, constant throughout the entire simulation time + molarMassWater = 18 #g/mol + molarMassSolute = 62 #g/mol root_conductivity = 1e-4 #1/d inner_kr = root_conductivity * r_root * 2 * 3.14 waterdemand = -0.05 #cm/d @@ -129,20 +121,9 @@ def run_perirhizal_test(): Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) Km = 1.5e-7 #mol/cm3 - #DS_W = 1.902e-5 #cm2/s - #Ds = DS_W / 10000#m2/s - Ds = 1.902e-5 / 2 #cm2/s # division by 2: from NO3 to H2PO4 - Ds = Ds * 24 * 3600 #cm2/d - - outer_waterpotential = initial_waterpotential #cm + #diffusion coefficient of nitrate + Ds = 1.902e-5 * 24 * 3600 #cm2/s -> cm2/d - outer_kr = root_conductivity * r_prhiz * 2 * 3.14 #TODO: this is just for testing purposes as I do not want a Dirichlet BC as it would be mixed BC - outer_conc = initial_soluteconcentration - - # the xylem matrix potential varies over time (keep it low so that there is little to no outflow of water) - rx_t = lambda t : -1000+200*np.sin(t) #cm - rx_t = lambda t : -14000+0*np.sin(t) #cm - rx_t = lambda t : -1000-200*t #cm # load the perirhizal model peri = PerirhizalPython() @@ -151,441 +132,260 @@ def run_perirhizal_test(): #no lookup tables are used here as there arent many simulations - #test the plots - Phi_soil = vg.fast_mfp[peri.sp](-100) - r_root = 0.02 - r_prhiz = 0.6 - r_eval = CC#np.linspace(r_root, r_prhiz, 100) - rootwateruptake = 0.2 - waterinflow = 0.05 - c_root = 0* 1e-7 - Ds0 = Ds - ss_uptake = 0*1.e-7 - sr_uptake = 1*1.e-7 - watercontent, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, r_eval, c_root, Ds0, ss_uptake, sr_uptake, sp) - print("watercontent", watercontent, "waterpotential", waterpotential, "soluteconcentration", soluteconcentration) - - c_bulk = 1e-6 - c_outer = 1.2e-6 - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil], [rootwateruptake], [waterinflow], [r_root], [r_prhiz], [c_bulk], [c_outer], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ff") - - #plt.plot(r_eval, soluteconcentration) -# - #plt.xlabel('X-axis') - #plt.ylabel('Y-axis') - #plt.title('Simple Plot') -# - #plt.show() - - - outer_watercontent = vg.water_content(outer_waterpotential, peri.sp) - - - simtimes = np.linspace(max_time/n_times,max_time,n_times).tolist() # days #TODO remove - - # initialise the dumux model - s = RichardsWrapper(RichardsNCCylFoam()) - s2 = RichardsWrapper(RichardsNCCylFoam()) - - s.initialize() - s.createGrid1d(np.linspace(r_root, r_prhiz, NC), length = length/100) # [m] -> [cm] - s.setVGParameters([soilVG]) - - # theta = 0.378, benchmark is set be nearly fully saturated, so we don't care too much about the specific values - s.setHomogeneousIC(initial_waterpotential) # cm pressure head - - s.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")# - #s.setTopBC("constantPressure",-200) # [cm/day] "noFlux")# - #s.setBotBC("constantFluxCyl",0.0) # "noFlux")# - s.setBotBC("constantFluxCyl",waterdemand) # "noFlux")# Flux in cm/d - s.setParameter("Soil.BC.Top.SType", "3") # michaelisMenten=8 (SType = Solute Type) - s.setParameter("Soil.BC.Top.CValue", "0.0") # michaelisMenten=8 (SType = Solute Type) - s.setParameter("Soil.BC.Bot.SType", "3") # michaelisMenten=8 (SType = Solute Type) - s.setParameter("Soil.BC.Bot.CValue", "0.0") - - s.setParameter("Soil.IC.C", str(initial_soluteconcentration)) # g / cm3 # TODO specialised setter? - - #s.setParameter("Component.MolarMass", "1.8e-2") - s.setParameter("Component.MolarMass", "62.0") - - s.setParameter("Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600) / 1000)) # m2 s-1 #TODO: this is not doing anything - s.initializeProblem(maxDt = 0.01) - - s2.initialize() - s2.createGrid1d(np.linspace(r_root, r_prhiz, NC), length = length/100) # [m] -> [cm] - s2.setVGParameters([soilVG]) - - # theta = 0.378, benchmark is set be nearly fully saturated, so we don't care too much about the specific values - s2.setHomogeneousIC(initial_waterpotential) # cm pressure head - - s2.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")# - #s.setTopBC("constantPressure",-200) # [cm/day] "noFlux")# - #s.setBotBC("constantFluxCyl",0.0) # "noFlux")# - s2.setBotBC("constantFluxCyl",waterdemand) # "noFlux")# Flux in cm/d - s2.setParameter("Soil.BC.Top.SType", "3") # michaelisMenten=8 (SType = Solute Type) - s2.setParameter("Soil.BC.Top.CValue", "0.0") # michaelisMenten=8 (SType = Solute Type) - s2.setParameter("Soil.BC.Bot.SType", "3") # michaelisMenten=8 (SType = Solute Type) - s2.setParameter("Soil.BC.Bot.CValue", "0.0") - - s2.setParameter("Soil.IC.C", str(initial_soluteconcentration)) # g / cm3 # TODO specialised setter? - #s.setParameter("Component.MolarMass", "1.8e-2") - s2.setParameter("Component.MolarMass", "62.0") - s2.setParameter("Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600))) # m2 s-1 - s2.initializeProblem(maxDt = 0.01) - - cellVolumes = s.getCellSurfacesCyl() * length # cm3 - print("cellVolumes",cellVolumes) - print(s) - - s.ddt = 1.e-4 # days - - simtimes.insert(0, 0) - dt_ = np.diff(simtimes) - - for r, dt in enumerate(dt_): - - dampening = 0.01 #slow down the inflow from outside + # initialise the dumux models for the scenarios + s_nf = RichardsWrapper(RichardsNCCylFoam()) #no flux outer BC + s_g = RichardsWrapper(RichardsNCCylFoam()) #Cauchy outer BC + + for s in [s_nf, s_g]: + s.initialize() + s.createGrid1d(np.linspace(r_root, r_prhiz, NC)/100, length = length/100) # [m] -> [cm] + s.setVGParameters([soilVG]) + s.setHomogeneousIC(initial_waterpotential) # cm pressure head + s.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")#default, will be changed for one scenario + s.setBotBC("constantFluxCyl",waterdemand) # "noFlux")# Flux in cm/d + s.setParameter("Soil.BC.Top.SType", "3") # constantFluxCyl=3 (SType = Solute Type) + s.setParameter("Soil.BC.Top.CValue", "0.0") + s.setParameter("Soil.BC.Bot.SType", "8") # michaelisMenten=8 (SType = Solute Type) + s.setParameter("Soil.BC.Bot.CValue", "0.0") #should not matter + s.setParameter("RootSystem.Uptake.Vmax", str(Vmax_per_area*molarMassSolute)) #mol/(cm2d) -> g/(cm2 d) + s.setParameter("RootSystem.Uptake.Km", str(Km*molarMassSolute)) # mol/cm3 -> g/cm3 + "RootSystem.Uptake.Vmax" + "RootSystem.Uptake.Km" + s.setParameter("Soil.IC.C", str(initial_soluteconcentration*molarMassSolute)) # g / cm3 # TODO specialised setter? + s.setParameter("Component.MolarMass", str(molarMassWater/1000)) #g/mol -> kg/mol water + s.setParameter("Component.MolarMass", str(molarMassSolute/1000)) #g/mol -> kg/mol nitrate + s.setParameter("1.Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600))) #cm^2/s -> m^2/s + s.initializeProblem(maxDt = 0.01) + s.ddt = 1.e-4 # days + + #in the general case the outer conditions are kept constant + s_g.setTopBC("constantPressure",initial_waterpotential) + s_g.setParameter("Soil.BC.Top.SType", "1") + s_g.setParameter("Soil.BC.Top.CValue", str(initial_soluteconcentration*molarMassSolute)) + + + cellVolumes = s_g.getCellSurfacesCyl() * length # cm3 + + #simtimes.insert(0, 0) + #dt_ = np.diff(simtimes) + + #variables for the no flux case + mean_watercontent_nf = 0.1 #cm3/cm3 + mean_waterpotential_nf = -100 #cm + mean_solutecontent_nf = 0 #mol/cm3 + Phi_soil_nf = 1.0 #mfp of the mean soil + Phi_root_nf = 1.0 #mfp next to the root + #mfp parameters for the perirhizal model + Phi_outer_nf = 1.0 #mfp at the outer perirhizal radius + rootuptake_w_nf = 1.0 #water uptake of the root, cm / d + inflow_w_nf = 0.0 #water uptake from outside the perirhizal zone, cm/d + rootuptake_s_nf = 1.0 #water uptake of the root, mol / cm2d + inflow_s_nf = 0.0 #water uptake from outside the perirhizal zone, mol / cm2d + + #variables for the general case + mean_watercontent_g = 0.1 #cm3/cm3 + mean_waterpotential_g = -100 #cm + mean_solutecontent_g = 0 #mol/cm3 + Phi_soil_g = 1.0 #mfp of the mean soil + Phi_root_g = 1.0 #mfp next to the root + #mfp parameters for the perirhizal model + Phi_outer_g = 1.0 #mfp at the outer perirhizal radius + rootuptake_w_g = 1.0 #water uptake of the root, cm / d + inflow_w_g = 0.0 #water uptake from outside the perirhizal zone, cm/d + rootuptake_s_g = 1.0 #water uptake of the root, mol / cm2d + inflow_s_g = 0.0 #water uptake from outside the perirhizal zone, mol / cm2d + + + for r, dt in enumerate(simtimes): time = simtimes[r] print('time',time) + print('no flux BC') + print("*****", "#", r, "external time step", dt, " d, simulation time", s_nf.simTime, "d, internal time step", s_nf.ddt, "d") + print('general') + print("*****", "#", r, "external time step", dt, " d, simulation time", s_g.simTime, "d, internal time step", s_g.ddt, "d") + + #one timestep + s_nf.solve(dt, saveInnerFluxes_ = True) + s_g.solve(dt, saveInnerFluxes_ = True) + + #watercontent and solute content, discretised + watercontent_nf = s_nf.getWaterContent() # cm3 + waterpotential_nf = s_nf.base.getPressureHead() # cm + watercontent_g = s_g.getWaterContent() # cm3 + waterpotential_g = s_g.base.getPressureHead() # cm + solutes_nf = s_nf.getSolution(1) / molarMassSolute # mol/cm3 + solutes_g = s_g.getSolution(1) / molarMassSolute # mol/cm3 - #if time >= 5: - # s.setSoluteTopBC([1], [0.]) - print("*****", "#", r, "external time step", dt, " d, simulation time", s.simTime, "d, internal time step", s.ddt, "d") - - Wvolbefore = cellVolumes * s.getWaterContent() # cm3 - Smassbefore = s.getSolution(1) * Wvolbefore # g - - #model root water uptake - rx = rx_t(r*dt) - - - current_potential = s.getSolutionHead() - water_dumux[r,1:] = vg.water_content(current_potential,peri.sp) - current_potential2 = s2.getSolutionHead() - water_dumux2[r,1:] = vg.water_content(current_potential2,peri.sp) - current_rs_potential = current_potential[0] - current_outer_potential = current_potential[-1] - current_outer_watercontent = vg.water_content(current_outer_potential, peri.sp) - root_wateruptake = inner_kr * (rx - current_rs_potential) - outer_watersource = outer_kr * (outer_waterpotential - current_outer_potential) - outer_watersource = (outer_watercontent - current_outer_watercontent) /dt * dampening - water_dumux[r,0] = root_wateruptake - print(water_dumux) - #s.setParameter( "Soil.BC.Bot.Value", str(root_wateruptake)) - - #alternative: adapt root matrix potential for a steady uptake: - rx = max(waterdemand / root_conductivity + current_rs_potential, -14750) #note: waterdemand is assumed to be negative - rx2 = max(waterdemand / root_conductivity + current_potential2[0], -14750) - #rx = current_rs_potential - - #model root solute uptake - #note: solutes are given in g - current_concentration = s.getSolution(1) - current_concentration2 = s2.getSolution(1) - solute_dumux[r,1:] = current_concentration - solute_dumux2[r,1:] = current_concentration - current_rs_concentration = current_concentration[0] - current_outer_concentration = current_concentration[-1] - root_soluteuptake = - Vmax * max(current_rs_concentration,0) / (Km + current_rs_concentration) # mol / d - root_soluteuptake2 = - Vmax * max(current_concentration2[0],0) / (Km + max(current_concentration2[0],0)) # mol / d - - outer_solutesource = ( outer_conc - current_outer_concentration ) * current_outer_watercontent / dt * dampening - solute_dumux[r,0] = root_soluteuptake - print(solute_dumux) - #s.setParameter( "Soil.BC.Bot.SValue", str(root_soluteuptake)) - #s.initializeProblem(maxDt = 0.01) - - s.setSource({NC-2: 100 * outer_watersource * volumes[-1] *length}) - s.setSource({0: root_soluteuptake*62*10, NC-2: outer_solutesource * volumes[-1] * length * 62}, eq_idx = 1) #factor 1000 is important for dumux as in the richards wrapper it is divided by 1000 - s2.setSource({0: root_soluteuptake2*1000}) - - s.solve(dt, saveInnerFluxes_ = True) - s2.solve(dt, saveInnerFluxes_ = True) - Wvolafter = cellVolumes*s.getWaterContent() # cm3 - Smassafter = s.getSolution(1) * Wvolafter # mol - - Wvolafter2 = cellVolumes*s2.getWaterContent() # cm3 - Smassafter2 = s.getSolution(1) * Wvolafter2 # mol - - - rootSoilFluxes = s.getInnerFlow(0, length) * dt # cm3 - rootSoilFluxesS = s.getInnerFlow(1, length) * dt # mol - soilSoilFluxes = s.getOuterFlow(0, length) * dt # cm3 - soilSoilFluxesS = s.getOuterFlow(1, length) * dt # mol - - rootwateruptake = abs(rootSoilFluxes) /length - #rootwateruptake = [abs(waterdemand)] #test TODO: remove this - waterinflow = np.array([abs(100*outer_watersource * volumes[-1])]) - - - - - # run the alpah omega model on waterflow (steady rate) - # mean water content of the rhizosphere - total_water = 0 - total_solute = 0 - total_water2 = 0 - total_solute2 = 0 - for j in range(NC-1): - total_water = total_water + water_dumux[r,j+1]*(points[j+1]**2 - points[j]**2) - total_solute = total_solute + water_dumux[r,j+1]*solute_dumux[r,j+1]*(points[j+1]**2 - points[j]**2) - total_water2 = total_water2 + water_dumux2[r,j+1]*(points[j+1]**2 - points[j]**2) - total_solute2 = total_solute2 + water_dumux2[r,j+1]*solute_dumux2[r,j+1]*(points[j+1]**2 - points[j]**2) - mean_water = total_water / (points[NC-1]**2 - points[0]**2) - total_solute = total_solute / (points[NC-1]**2 - points[0]**2) - mean_solute = total_solute / mean_water - mean_water2 = total_water2 / (points[NC-1]**2 - points[0]**2) - total_solute2 = total_solute2 / (points[NC-1]**2 - points[0]**2) - mean_solute2 = total_solute2 / mean_water - print("mean solute",mean_solute) - - #translate the mean water content to a mean matrix potential - sx = vg.pressure_head(mean_water, peri.sp) - sx2 = vg.pressure_head(mean_water2, peri.sp) - - #start the steady rate solver - h_sr = peri.soil_root_interface_(rx, sx, inner_kr, rho, peri.sp) - h_sr2 = peri.soil_root_interface_(rx2, sx2, inner_kr, rho, peri.sp) - - #compute both matrix flux potentials: - Phi_root = np.array(vg.fast_mfp[sp](h_sr)) - Phi_soil = np.array(vg.fast_mfp[sp](sx)) - Phi_soil_dumux = Phi_soil - - Phi_root2 = np.array(vg.fast_mfp[sp](h_sr2)) - Phi_soil2 = np.array(vg.fast_mfp[sp](sx2)) - - #compute the spatial watercontents - Phi_A, Phi_C = peri.determine_mfp_function(Phi_root, Phi_soil, rho) - Phi_A2, Phi_C2 = peri.determine_mfp_function(Phi_root2, Phi_soil2, rho) - - #alternative computation using the waterdemand - Phi_A = (abs(waterdemand)*r_root)*((rho)**2/(2*(1-rho**2))) #TODO. why *10? - Phi_A2 = (abs(waterdemand)*r_root)*((rho)**2/(2*(1-rho**2))) - #Phi_C = Phi_soil - Phi_A #inaccurate, as the mean mfp will then only be reached at the outer boundary - Phi_C = Phi_soil - (0.53**2+2*np.log(1/0.53))*Phi_A - Phi_C2 = Phi_soil2 - (0.53**2+2*np.log(1/0.53))*Phi_A2 - - Phi_root_srnf = Phi_A*(1/rho**2+2*np.log(rho))+Phi_C - Phi_outer_srnf = Phi_A + Phi_C - Phi_root_srnf2 = Phi_A2*(1/rho**2+2*np.log(rho))+Phi_C2 - Phi_outer_srnf2 = Phi_A2 + Phi_C2 - - #if waterstress, then Phi_A = - rho**2*Phi_C - if Phi_A*(1/rho**2-np.log(1/rho**2))+Phi_C <=0: - Phi_A = Phi_soil*rho**2/(rho**2-1) - Phi_C = -Phi_A*(1/rho**2-np.log(1/rho**2)) - - Phi_A_orig = Phi_A - Phi_C_orig = Phi_C - #compute Phi_A based on the water demand - #Phi_A = waterdemand * r_root / (2*(1-rho**2)) - #Phi_C = - #compute Phi_C based on the result - - #outer Phi - Phi_out = Phi_A+Phi_C - print("Phiroot", Phi_root, "Phi_soil", Phi_soil, "Phi_A", Phi_A, "Phi_C", Phi_C) - r_rel = CC[0] / r_prhiz - print("Phi_out", Phi_out, "Phi_in", Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C) - - #determine the critical matrix potential - h_out = [-200, -800, -3200, -6000,-10000,-13700, -14000] # cm - r_rel = 1/rho - MFP_root = lambda Phi : Phi+abs(waterdemand)*r_root*((r_rel*rho)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) - MFP_root_stress = lambda Phi : Phi*((r_rel*rho)**2 - 1 + 2*rho**2*np.log(1/(r_rel*rho))/(rho**2 - 1 + 2*rho**2*np.log(1/rho))) - MFP_mean_difference = lambda Phi : Phi+abs(waterdemand)*r_root*((0.53*rho)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) - Phi_soil - print(MFP_root_stress(vg.fast_mfp[sp](h_out[0])),MFP_root_stress(vg.fast_mfp[sp](h_out[-1]))) - if MFP_mean_difference(vg.fast_mfp[sp](h_out[0]))*MFP_mean_difference(vg.fast_mfp[sp](h_out[-1]))<0: - MFP_out = root_scalar(MFP_mean_difference, method="brentq", bracket=[ vg.fast_mfp[sp](h_out[0]), vg.fast_mfp[sp](h_out[-1])]).root - else: - MFP_out = 1.e-3 - print(MFP_out) - #MFP_out = MFP_out /10000 - - r_rel = CC[0] / r_prhiz - Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C - mp_perirhizal1[r,0]=vg.fast_imfp[sp](Phi) - Phi2 = Phi_A2*(r_rel**2-np.log(r_rel**2))+Phi_C2 - mp_perirhizal2[r,0]=vg.fast_imfp[sp](Phi2) - for j in range(NC-1): - r_rel = CC[j] / r_prhiz - Phi = Phi_A*(r_rel**2-np.log(r_rel**2))+Phi_C - mp_perirhizal1[r,j+1]=vg.fast_imfp[sp](Phi) # TODO: adapt this to stress maybe? - Phi2 = Phi_A2*(r_rel**2-np.log(r_rel**2))+Phi_C2 - mp_perirhizal2[r,j+1]=vg.fast_imfp[sp](Phi2) # TODO: adapt this to stress maybe? - - #Phi_soil = Phi_out # TODO: remove this - #if water_dumux[r,j+1] Date: Sun, 5 Jul 2026 00:28:15 +0200 Subject: [PATCH 29/41] correct Vmax value for the solute uptake --- .../test_perirhizal.py | 223 ++++++++++++------ src/functional/Perirhizal.py | 23 +- 2 files changed, 169 insertions(+), 77 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index bf2b62b94..c28b5035d 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -35,7 +35,7 @@ # run the dumux implementation of root water and nitrate uptake, later compare it to the analytical approximation n_tests = 1 #try everything here for this many random parameter sets -do_computation = True #should the computation be run or take the data from a saved file +do_computation = False #should the computation be run or take the data from a saved file # general parameters max_time = 10 #d @@ -45,11 +45,11 @@ NC = 40 # number of spatial discretisations length = 1 #default length of the segment, will not change the outcpme as all variables are constant in this direction [cm] -#two scenarios will be computed: one without inflow, another with a Cauchy BC +#two scenarios will be computed: one without inflow, another with a Dirichlet BC n_scenarios = 2 #initial conditions -initial_waterpotential = -100 +initial_waterpotential = -200 initial_soluteconcentration = 2e-5#mol/cm3 #space for the outputs @@ -75,16 +75,16 @@ +#discretisation +lb = 0.5 +points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), NC+1, base = lb) +CC = np.array([(points[i] + points[i+1])/2 for i in range(NC)]) +volumes = np.array([(points[i+1]**2 - points[i]**2)*3.14 for i in range(NC)]) - - - -outer_conc = initial_soluteconcentration - -def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenarios, initial_waterpotential, initial_soluteconcentration): +def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volumes, length, n_scenarios, initial_waterpotential, initial_soluteconcentration): #space for the outputs watercontent_dumux = np.zeros((n_scenarios, n_times, NC+2)) #watercontent in cm3/cm3, (radial) uptake and inflow in cm/d @@ -96,7 +96,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar solutes_dumux_ss = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) solutes_dumux_ff = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) - simtimes = np.linspace(0,max_time,n_times)[1:] + simtimes = np.linspace(0,max_time,n_times+1)[1:] dt = max_time / n_times rho = r_prhiz / r_root @@ -104,10 +104,10 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil #discretisation - lb = 0.5 - points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), NC+1, base = lb) - CC = np.array([(points[i] + points[i+1])/2 for i in range(NC)]) - volumes = np.array([(points[i+1]**2 - points[i]**2)*3.14 for i in range(NC)]) + #lb = 0.5 + #points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), NC+1, base = lb) + #CC = np.array([(points[i] + points[i+1])/2 for i in range(NC)]) + #volumes = np.array([(points[i+1]**2 - points[i]**2)*3.14 for i in range(NC)]) # root conductivity and solute uptake parameters, constant throughout the entire simulation time @@ -140,13 +140,19 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar for s in [s_nf, s_g]: s.initialize() - s.createGrid1d(np.linspace(r_root, r_prhiz, NC)/100, length = length/100) # [m] -> [cm] + s.createGrid1d(points, length = length/100) # [m] -> [cm] s.setVGParameters([soilVG]) s.setHomogeneousIC(initial_waterpotential) # cm pressure head - s.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")#default, will be changed for one scenario + s.setBotBC("constantFluxCyl",waterdemand) # "noFlux")# Flux in cm/d - s.setParameter("Soil.BC.Top.SType", "3") # constantFluxCyl=3 (SType = Solute Type) - s.setParameter("Soil.BC.Top.CValue", "0.0") + if s == s_nf: + s.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")#default, will be changed for one scenario + s.setParameter("Soil.BC.Top.SType", "3") # constantFluxCyl=3 (SType = Solute Type) + s.setParameter("Soil.BC.Top.CValue", "0.0") + else: + s.setTopBC("constantPressure",initial_waterpotential) + s.setParameter("Soil.BC.Top.SType", "10") #advective flow + s.setParameter("Soil.BC.Top.CValue", str(initial_soluteconcentration*molarMassSolute)) s.setParameter("Soil.BC.Bot.SType", "8") # michaelisMenten=8 (SType = Solute Type) s.setParameter("Soil.BC.Bot.CValue", "0.0") #should not matter s.setParameter("RootSystem.Uptake.Vmax", str(Vmax_per_area*molarMassSolute)) #mol/(cm2d) -> g/(cm2 d) @@ -155,21 +161,19 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar "RootSystem.Uptake.Km" s.setParameter("Soil.IC.C", str(initial_soluteconcentration*molarMassSolute)) # g / cm3 # TODO specialised setter? s.setParameter("Component.MolarMass", str(molarMassWater/1000)) #g/mol -> kg/mol water - s.setParameter("Component.MolarMass", str(molarMassSolute/1000)) #g/mol -> kg/mol nitrate + s.setParameter("1.Component.MolarMass", str(molarMassSolute/1000)) #g/mol -> kg/mol nitrate s.setParameter("1.Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600))) #cm^2/s -> m^2/s s.initializeProblem(maxDt = 0.01) s.ddt = 1.e-4 # days #in the general case the outer conditions are kept constant - s_g.setTopBC("constantPressure",initial_waterpotential) - s_g.setParameter("Soil.BC.Top.SType", "1") - s_g.setParameter("Soil.BC.Top.CValue", str(initial_soluteconcentration*molarMassSolute)) + #s_g.setTopBC("constantPressure",initial_waterpotential) + #s_g.setParameter("Soil.BC.Top.SType", "1") #Dirichlet BC + #s_g.setParameter("Soil.BC.Top.SType", "10") #advective flow + #s_g.setParameter("Soil.BC.Top.CValue", str(initial_soluteconcentration*molarMassSolute)) cellVolumes = s_g.getCellSurfacesCyl() * length # cm3 - - #simtimes.insert(0, 0) - #dt_ = np.diff(simtimes) #variables for the no flux case mean_watercontent_nf = 0.1 #cm3/cm3 @@ -198,9 +202,12 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar inflow_s_g = 0.0 #water uptake from outside the perirhizal zone, mol / cm2d - for r, dt in enumerate(simtimes): - - time = simtimes[r] + for r, time in enumerate(simtimes): + + dt = time + if r>0: + dt = time - simtimes[r-1] + print('time',time) print('no flux BC') print("*****", "#", r, "external time step", dt, " d, simulation time", s_nf.simTime, "d, internal time step", s_nf.ddt, "d") @@ -213,9 +220,9 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar #watercontent and solute content, discretised watercontent_nf = s_nf.getWaterContent() # cm3 - waterpotential_nf = s_nf.base.getPressureHead() # cm + waterpotential_nf = np.array([vg.pressure_head(watercontent_nf[i],peri.sp) for i in range(NC)]) # cm watercontent_g = s_g.getWaterContent() # cm3 - waterpotential_g = s_g.base.getPressureHead() # cm + waterpotential_g = np.array([vg.pressure_head(watercontent_g[i],peri.sp) for i in range(NC)]) # cm solutes_nf = s_nf.getSolution(1) / molarMassSolute # mol/cm3 solutes_g = s_g.getSolution(1) / molarMassSolute # mol/cm3 @@ -230,6 +237,15 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar inflow_w_g = s_g.getOuterFlow(0, length) /(length*(2*np.pi*r_prhiz)) # cm /d inflow_s_g = s_g.getOuterFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_prhiz)) # mol / cm2d + rootuptake_w_nf = rootuptake_w_nf[0] + rootuptake_s_nf = rootuptake_s_nf[0] + inflow_w_nf = inflow_w_nf[0] + inflow_s_nf = inflow_s_nf[0] + rootuptake_w_g = rootuptake_w_g[0] + rootuptake_s_g = rootuptake_s_g[0] + inflow_w_g = inflow_w_g[0] + inflow_s_g = inflow_s_g[0] + #store the dumux outputs #scenario no flux outer BC watercontent_dumux[0,r,0]=rootuptake_w_nf @@ -269,13 +285,13 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar #both q_root and q_out are assumed to have positive signs if the water flows to the root #no flux outer BC Phi_soil_nf = vg.fast_mfp[peri.sp](mean_waterpotential_nf) #mfp of the mean soil - Phi_outer_nf = Phi_soil_nf - (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.ln(0.53))-(inflow_w_nf*r_prhiz)*r_prhiz*np.ln(0.53) #mfp at the outer perirhizal radius - Phi_nf = lambda r: Phi_soil_nf + (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.ln(r/r_prhiz))+(inflow_w_nf*r_prhiz)*r_prhiz*np.ln(r/r_prhiz)#mfp function depending on radius + Phi_outer_nf = Phi_soil_nf - (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_nf*r_prhiz)*r_prhiz*np.log(0.53) #mfp at the outer perirhizal radius + Phi_nf = lambda r: Phi_soil_nf + (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_nf*r_prhiz)*r_prhiz*np.log(r/r_prhiz)#mfp function depending on radius Phi_root_nf = Phi_nf(r_root)#mfp next to the root #general outer BC: Dirichlet BC to a fixed potential Phi_soil_g = vg.fast_mfp[peri.sp](mean_waterpotential_g) #mfp of the mean soil - Phi_outer_g = Phi_soil_g - (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.ln(0.53))-(inflow_w_g*r_prhiz)*r_prhiz*np.ln(0.53) #mfp at the outer perirhizal radius - Phi_g = lambda r: Phi_soil_g + (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.ln(r/r_prhiz))+(inflow_w_g*r_prhiz)*r_prhiz*np.ln(r/r_prhiz)#mfp function depending on radius + Phi_outer_g = Phi_soil_g - (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_g*r_prhiz)*r_prhiz*np.log(0.53) #mfp at the outer perirhizal radius + Phi_g = lambda r: Phi_soil_g + (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_g*r_prhiz)*r_prhiz*np.log(r/r_prhiz)#mfp function depending on radius Phi_root_g = Phi_nf(r_root)#mfp next to the root #write the steady rate approximations for the water as outputs @@ -298,15 +314,16 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar #case of dumux no flux outer BC #use the outer concentration for the steady state approximation here, even if that is not necessarily known in the macroscopic model #(the outer concentration can be computed as a mean of all surrounding voxels? - result_solutes_ss_nf = peri.soil_root_solutes_ss_([Phi_root_nf], [Phi_outer_nf], [solutes_nf[-1]], [Vmax], [Km], Ds, [-abs(radial_waterdemand)], peri.sp) - result_solutes_sr_nf = peri.soil_root_solutes_sr_([Phi_root_nf], [Phi_outer_nf], [rho], [mean_soluteconcent_nf], [Vmax], [Km], Ds, [-abs(radial_waterdemand)], peri.sp) + #TODO: check weather Vmax or Vmax per area + result_solutes_ss_nf = peri.soil_root_solutes_ss_([Phi_root_nf], [Phi_outer_nf], [solutes_nf[-1]], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) + result_solutes_sr_nf = peri.soil_root_solutes_sr_([Phi_root_nf], [Phi_outer_nf], [rho], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) #safe the results #(here there is no computed inflow, so index 1 doesn't do anything) result_solutes_ss_nf = result_solutes_ss_nf[0] - solutes_dumux_ss[0,r,0]=-Vmax * result_solutes_ss_nf / (Km + result_solutes_ss_nf) + solutes_dumux_ss[0,r,0]=-Vmax_per_area * result_solutes_ss_nf / (Km + result_solutes_ss_nf) result_solutes_sr_nf = result_solutes_sr_nf[0] - solutes_dumux_sr[0,r,0]=-Vmax * result_solutes_sr_nf / (Km + result_solutes_sr_nf) + solutes_dumux_sr[0,r,0]=-Vmax_per_area * result_solutes_sr_nf / (Km + result_solutes_sr_nf) F0_nf = peri.integral_overDiffusion_(Phi_root_nf,peri.sp) F0_g = peri.integral_overDiffusion_(Phi_root_g,peri.sp) @@ -318,25 +335,32 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar F_g = peri.integral_overDiffusion_(Phi_g(r_current),peri.sp)-F0_g F_tilde_nf=math.exp(D_tilde*F_nf) F_tilde_g=math.exp(D_tilde*F_g) - solutes_dumux_ss[0,r,2+j] = result_solutes_ss_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_ss[0,r,0] / abs(radial_waterdemand)#waterdemand is assumed to be negative - solutes_dumux_sr[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_sr[0,r,0] / abs(radial_waterdemand) #an uptake of both water and solute is assumed + solutes_dumux_ss[0,r,2+j] = result_solutes_ss_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_ss[0,r,0] / radial_waterdemand#waterdemand is assumed to be negative #TODO: look at the signs + solutes_dumux_sr[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_sr[0,r,0] / radial_waterdemand #an uptake of both water and solute is assumed #case of general steady rate water uptake #for the steady state take again the outer concentration - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ss") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) - solutes_dumux_ss[1,r,0] = -Uptake + #TODO: check weather Vmax or Vmax per area + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr_ss") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + solutes_dumux_ss[1,r,0] = -Uptake[0] + solutes_dumux_ss[1,r,1] = -ss_uptake[0] solutes_dumux_ss[1,r,2:] = soluteconcentration[:] + solutes_dumux_ss[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate no flux outer BC - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr_nf") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) - solutes_dumux_sr[1,r,0] = -Uptake + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [-inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr_nf") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + solutes_dumux_sr[1,r,0] = -Uptake[0] + solutes_dumux_sr[1,r,1] = -ss_uptake[0] solutes_dumux_sr[1,r,2:] = soluteconcentration[:] + solutes_dumux_sr[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate solute uptake with the farfield approximation - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr_ff") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, r_eval, rsc, Ds, ss_uptake, sr_uptake, peri.sp) - solutes_dumux_ff[1,r,0] = -Uptake + rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [-inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr_ff") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + solutes_dumux_ff[1,r,0] = -Uptake[0] + solutes_dumux_ff[1,r,1] = -ss_uptake[0] solutes_dumux_ff[1,r,2:] = soluteconcentration[:] + solutes_dumux_ff[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area return watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_dumux_sr, solutes_dumux_ss, solutes_dumux_ff @@ -345,16 +369,15 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar if do_computation: # save everything in the np arrays for i in range(n_tests): - watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_dumux_sr, solutes_dumux_ss, solutes_dumux_ff = run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenarios, initial_waterpotential, initial_soluteconcentration) - watercontent_dumux[i,:,:,:] = water_dumux[:,:,:] - watercontent_dumux[i,:,:,:] = watercontent_dumux[:,:,:] - waterpotential_dumux[i,:,:,:] = waterpotential_dumux[:,:,:] - watercontent_sr[i,:,:,:] = watercontent_sr[:,:,:] - waterpotential_sr[i,:,:,:] = waterpotential_sr[:,:,:] - solutes_dumux[i,:,:,:] = solutes_dumux[:,:,:] - solutes_dumux_sr[i,:,:,:] = solutes_dumux_sr[:,:,:] - solutes_dumux_ss[i,:,:,:] = solutes_dumux_ss[:,:,:] - solutes_dumux_ff[i,:,:,:] = solutes_dumux_ff[:,:,:] + watercontent_dumux[i,:,:,:], waterpotential_dumux[i,:,:,:], watercontent_sr[i,:,:,:], waterpotential_sr[i,:,:,:], solutes_dumux[i,:,:,:], solutes_dumux_sr[i,:,:,:], solutes_dumux_ss[i,:,:,:], solutes_dumux_ff[i,:,:,:] = run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volumes, length, n_scenarios, initial_waterpotential, initial_soluteconcentration) + #watercontent_dumux[i,:,:,:] = watercontent_dumux[:,:,:] + #waterpotential_dumux[i,:,:,:] = waterpotential_dumux[:,:,:] + #watercontent_sr[i,:,:,:] = watercontent_sr[:,:,:] + #waterpotential_sr[i,:,:,:] = waterpotential_sr[:,:,:] + #solutes_dumux[i,:,:,:] = solutes_dumux[:,:,:] + #solutes_dumux_sr[i,:,:,:] = solutes_dumux_sr[:,:,:] + #solutes_dumux_ss[i,:,:,:] = solutes_dumux_ss[:,:,:] + #solutes_dumux_ff[i,:,:,:] = solutes_dumux_ff[:,:,:] np.savez("test_perirhizal.npz", watercontent_dumux=watercontent_dumux, @@ -371,6 +394,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar watercontent_dumux = simulation_results["watercontent_dumux"] waterpotential_dumux = simulation_results["waterpotential_dumux"] watercontent_sr = simulation_results["watercontent_sr"] + waterpotential_sr = simulation_results["waterpotential_sr"] solutes_dumux = simulation_results["solutes_dumux"] solutes_dumux_sr = simulation_results["solutes_dumux_sr"] solutes_dumux_ss = simulation_results["solutes_dumux_ss"] @@ -402,10 +426,12 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar # left: sr no flux, ss for the no flux # right: ss, sr, farfield for sr waterflow -ax2_0 = ax1[i,0].twinx() -ax2_1 = ax1[i,1].twinx() + for i in range(5): + ax2_0 = ax1[i,0].twinx() + ax2_1 = ax1[i,1].twinx() + #load data water_dumux_nf = waterpotential_dumux[run, 0, timestep[i], 2:] water_dumux_g = waterpotential_dumux[run, 1, timestep[i], 2:] @@ -422,18 +448,20 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar #left plot: no flux outer BC ax1[i,0].plot(CC, water_dumux_nf, "b", linestyle = linestyle_dumux, label = "water_dumux") ax1[i,0].plot(CC, water_steadyrate_nf, "b", linestyle = linestyle_steadyrate, label = "water_sr") - ax2_0.plot(CC, solute_dumux_nf, "b", linestyle = linestyle_dumux, label = "solute_dumux") - ax2_0.plot(CC, solute_ss_nf, "b", linestyle = linestyle_steadystate, label = "solute_ss") - ax2_0.plot(CC, solute_sr_nf, "b", linestyle = linestyle_steadyrate, label = "solute_sr") + ax2_0.plot(CC, solute_dumux_nf, "m", linestyle = linestyle_dumux, label = "solute_dumux") + ax2_0.plot(CC, solute_ss_nf, "m", linestyle = linestyle_steadystate, label = "solute_ss") + ax2_0.plot(CC, solute_sr_nf, "m", linestyle = linestyle_steadyrate, label = "solute_sr") #right plot: Dirichlet (initial conditions) outer BC ax1[i,1].plot(CC, water_dumux_g, "b", linestyle = linestyle_dumux, label = "water_dumux") ax1[i,1].plot(CC, water_steadyrate_g, "b", linestyle = linestyle_steadyrate, label = "water_sr") - ax2_1.plot(CC, solute_dumux_g, "b", linestyle = linestyle_dumux, label = "solute_dumux") - ax2_1.plot(CC, solute_ss_g, "b", linestyle = linestyle_steadystate, label = "solute_ss") - ax2_1.plot(CC, solute_sr_g, "b", linestyle = linestyle_steadyrate, label = "solute_sr") - ax2_1.plot(CC, solute_ff_g, "b", linestyle = linestyle_steadyrate, label = "solute_sr") - + ax2_1.plot(CC, solute_dumux_g, "m", linestyle = linestyle_dumux, label = "solute_dumux") + ax2_1.plot(CC, solute_ss_g, "m", linestyle = linestyle_steadystate, label = "solute_ss") + ax2_1.plot(CC, solute_sr_g, "m", linestyle = linestyle_steadyrate, label = "solute_sr") + ax2_1.plot(CC, solute_ff_g, "m", linestyle = linestyle_special, label = "solute_ff") + #print(solute_ss_g) + #print(solute_sr_g) + print(solutes_dumux[run, 1, timestep[i], :]) ax1[i,0].set_xlabel("distance root [cm]") ax1[i,0].set_ylabel("water") ax2_0.set_ylabel("nitrogen") @@ -457,5 +485,62 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, length, n_scenar +fig, ax1 = figure_style.subplots12(nrows=1, ncols=2) +# dumux(both) +# left: sr no flux, ss for the no flux +# right: ss, sr, farfield for sr waterflow + +suptake_dumux_nf = solutes_dumux[run, 0, 1:, 0] +suptake_dumux = solutes_dumux[run, 1, 1:, 0] + +solute_sr_nf = solutes_dumux_sr[run, 0, 1:, 0] +solute_ss_nf = solutes_dumux_ss[run, 0, 1:, 0] +solute_sr = solutes_dumux_sr[run, 1, 1:, 0] +solute_ss = solutes_dumux_ss[run, 1, 1:, 0] +solute_ff = solutes_dumux_ff[run, 1, 1:, 0] + +#TODO: test segment here, remove +solute_sr_nf_conc = solutes_dumux_sr[run, 0, 1:, 2] +solute_ss_nf_conc = solutes_dumux_ss[run, 0, 1:, 2] +solute_sr_conc = solutes_dumux_sr[run, 1, 1:, 2] +solute_ss_conc = solutes_dumux_ss[run, 1, 1:, 2] +solute_ff_conc = solutes_dumux_ff[run, 1, 1:, 2] + +Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d +Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) +Km = 1.5e-7 #mol/cm3 + +for i in range(len(solute_sr_nf_conc)): + solute_sr_nf[i]=-Vmax_per_area*solute_sr_nf_conc[i]/(Km+solute_sr_nf_conc[i]) + solute_ss_nf[i]=-Vmax_per_area*solute_ss_nf_conc[i]/(Km+solute_ss_nf_conc[i]) + solute_sr[i]=-Vmax_per_area*solute_sr_conc[i]/(Km+solute_sr_conc[i]) + solute_ss[i]=-Vmax_per_area*solute_ss_conc[i]/(Km+solute_ss_conc[i]) + solute_ff[i]=-Vmax_per_area*solute_ff_conc[i]/(Km+solute_ff_conc[i]) + +#print(suptake_dumux) +#print(solute_ss) +#print(solutes_dumux_ss[run, 1, 1:, 2]) +ax1[0].plot(suptake_dumux_nf, suptake_dumux_nf, "m", linestyle = linestyle_dumux, label = "dumux") +ax1[0].plot(suptake_dumux_nf, abs(solute_sr_nf), "m", linestyle = linestyle_steadyrate, label = "steady rate nf") +ax1[0].plot(suptake_dumux_nf, abs(solute_ss_nf), "m", linestyle = linestyle_steadystate, label = "steady state nf") +ax1[0].scatter(suptake_dumux_nf, abs(solute_ss_nf), marker = "*") +ax1[1].plot(suptake_dumux, suptake_dumux, "m", linestyle = linestyle_dumux, label = "dumux") +ax1[1].scatter(suptake_dumux, abs(suptake_dumux), marker = "*") +ax1[1].plot(suptake_dumux, abs(solute_sr), "m", linestyle = linestyle_steadyrate, label = "steady rate") +ax1[1].scatter(suptake_dumux, abs(solute_sr), marker = "*") +ax1[1].plot(suptake_dumux, abs(solute_ss), "m", linestyle = linestyle_steadystate, label = "steady state") +ax1[1].scatter(suptake_dumux, abs(solute_ss), marker = "*") +ax1[1].plot(suptake_dumux, abs(solute_ff), "m", linestyle = linestyle_special, label = "far field approximation") +ax1[1].scatter(suptake_dumux, abs(solute_ff), marker = "*") + +ax1[0].set_xlabel("dumux solute uptake") +ax1[0].set_ylabel("analytical approximation") +ax1[0].legend(["solute utake mol/cm2d"], loc="upper left") + +ax1[1].set_xlabel("dumux solute uptake") +ax1[1].set_ylabel("analytical approximation") +ax1[1].legend(["solute utake mol/cm2d"], loc="upper left") +#np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +plt.show() diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 7374378da..e0face78d 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -113,6 +113,11 @@ def soil_root_interface_potentials(self, rx, sx, inner_kr, rho): """ assert len(rx) == len(sx) == len(inner_kr) == len(rho), "rx, sx, inner_kr, and rho must have the same length" + for i in range(len(rx)): + if rx[i] < -15989: + rx[i] = -15989 + if sx[i] > -1: + sx[i] = -1 if self.lookup_table: rsx = self.soil_root_interface_potentials_table(rx, sx, inner_kr, rho) @@ -298,7 +303,7 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): rho = r_prhiz[i] / r_root[i] - print("rootwateruptake",rootwateruptake[i],waterinflow[i]) + #print("rootwateruptake",rootwateruptake[i],waterinflow[i]) Phi_out = Phi_soil[i] -( (rootwateruptake[i]-waterinflow[i])*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow[i]*np.log(0.53) ) #Phi_out = root_scalar(Phi_diff, method="brentq", bracket=[1e-5, Phi_soil]).root Phi = lambda r_rel : Phi_out + (rootwateruptake[i]-waterinflow[i])*((r_rel**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) #TODO: test using Phi_soil instead of Phi_outer @@ -306,7 +311,7 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel)),sp),10/3) / (sp.theta_S**2) # Millington and Quirk #values for the subfunction - print("Phi", Phi(1e-10), Phi(r_prhiz[i])) + #print("Phi", Phi(1e-10), Phi(r_prhiz[i])) # = root_scalar(Phi, method="brentq", bracket=[1e-10, r_prhiz[i]]).root r_crit = r_root[i] / 100 @@ -417,7 +422,8 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, if p>0: rsc[i] = - p/2 + math.sqrt(p**2/4-q) #only the positive solution makes sense #test for negative else: - rsc[i] = - p/2 - math.sqrt(p**2/4-q) #this is the standard case as m[3] will usually be negative + rsc[i] = - p/2 + math.sqrt(p**2/4-q) #this is the standard case as m[3] will usually be negative + # TODO: twice the same sign in the lines before Uptake[i] = m[3]*rsc[i]+n[3] ss_uptake[i] = m[0]*rsc[i]+n[0] sr_uptake[i] = m[1]*rsc[i]+n[1] @@ -469,7 +475,7 @@ def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_pr waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) - print("Phi_root",Phi(1e-10), Phi(r_prhiz)) + #print("Phi_root",Phi(1e-10), Phi(r_prhiz)) #r_crit = root_scalar(Phi, method="brentq", bracket=[1e-10,r_prhiz]).root r_crit = r_root / 100 #solutes @@ -599,7 +605,7 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf rsc = np.zeros(n_segments) F = np.zeros(n_segments) #F is a helper values F_tilde_inv = np.zeros(n_segments) - + #print("test",Phi_root[0], Phi_soil[0], c_bulk, Vmax, Km, Ds, waterflow) #TODO remove if self.lookup_table_solutes: F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, n_segments)] else: @@ -609,9 +615,9 @@ def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterf D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) #solve quadratic eqation # TODO: Link to publication - print("test",Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow) #TODO remove + for i in range(n_segments): - print("Dtilde",D_tilde,"F",F[i]) + #print("Dtilde",D_tilde,"F",F[i]) F_tilde_inv[i]=math.exp(-D_tilde*F[i]) a1=c_bulk[i]*F_tilde_inv[i] a2=(1-F_tilde_inv[i])/(waterflow[i]) @@ -676,7 +682,7 @@ def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, w #reuse steady state case #solve quadratic eqation # TODO: Link to publication for i in range(n_segments): - print("C_d",C_d,"F",F[i]) + #print("C_d",C_d,"F",F[i]) F_tilde_inv[i]=math.exp(-C_d*F[i]) a1=(c_bulk[i]*watercontent[i])/(R_sr[i])*F_tilde_inv[i] a2=(1-watercontent[i]/R_sr[i]*F_tilde_inv[i])/waterflow[i] @@ -701,6 +707,7 @@ def integral_overDiffusion_(Phi, sp): if Phi <=0: return 0 theta_rel = sp.theta_R/(sp.theta_S-sp.theta_R) + #print("Phi",Phi) integral_fun = lambda Phi: pow(theta_rel+vg.effective_saturation(vg.fast_imfp[sp](Phi),sp),-13/3) integral_overD, _ = integrate.quad(integral_fun, 1.0e-3, Phi) From f797a86831b3b46394509b6ab73c0a51f34930ae Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sun, 5 Jul 2026 20:36:50 +0200 Subject: [PATCH 30/41] simplified steady rate implementaiton --- .../test_perirhizal.py | 12 +-- src/functional/Perirhizal.py | 80 +++++++++++++++---- 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index c28b5035d..4cf5b057a 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -38,8 +38,8 @@ do_computation = False #should the computation be run or take the data from a saved file # general parameters -max_time = 10 #d -n_times = 1000 # number of time intervals +max_time = 0.1 #d +n_times = 10 # number of time intervals r_prhiz = 0.6 # perirhizal radius[cm] r_root = 0.02 # root radius [cm] NC = 40 # number of spatial discretisations @@ -315,9 +315,11 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #use the outer concentration for the steady state approximation here, even if that is not necessarily known in the macroscopic model #(the outer concentration can be computed as a mean of all surrounding voxels? #TODO: check weather Vmax or Vmax per area - result_solutes_ss_nf = peri.soil_root_solutes_ss_([Phi_root_nf], [Phi_outer_nf], [solutes_nf[-1]], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) - result_solutes_sr_nf = peri.soil_root_solutes_sr_([Phi_root_nf], [Phi_outer_nf], [rho], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) + #result_solutes_ss_nf = peri.soil_root_solutes_ss_([Phi_root_nf], [Phi_outer_nf], [solutes_nf[-1]], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) + #result_solutes_sr_nf = peri.soil_root_solutes_sr_([Phi_root_nf], [Phi_outer_nf], [rho], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) + result_solutes_ss_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp, n_approx = 5) + result_solutes_sr_nf = result_solutes_ss_nf #safe the results #(here there is no computed inflow, so index 1 doesn't do anything) result_solutes_ss_nf = result_solutes_ss_nf[0] @@ -461,7 +463,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu ax2_1.plot(CC, solute_ff_g, "m", linestyle = linestyle_special, label = "solute_ff") #print(solute_ss_g) #print(solute_sr_g) - print(solutes_dumux[run, 1, timestep[i], :]) + #print(solutes_dumux[run, 1, timestep[i], :]) ax1[i,0].set_xlabel("distance root [cm]") ax1[i,0].set_ylabel("water") ax2_0.set_ylabel("nitrogen") diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index e0face78d..9948045da 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -6,6 +6,7 @@ from scipy.spatial import ConvexHull, Voronoi from scipy.integrate import odeint from scipy import integrate +from scipy.special.orthogonal import ts_roots import time #only used for a test @@ -584,43 +585,94 @@ def soil_root_solutes_combined(self, Phi_soil, rootwateruptake, soilwateruptake, return 1 - def soil_root_solutes_ss_(self, Phi_root, Phi_soil, c_bulk, Vmax, Km, Ds, waterflow, sp): + def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r_prhiz, c_bulk, Vmax, Km, Ds, waterflow, sp, n_approx = 1): """ - finds solute concentration at the soil root interface for all segments assuming steady state - uses a look up tables if present (see create_lookup, and open_lookup) + finds solute concentration at the soil root interface for all segments assuming that the waterflow and soluteflow are proportional throughout the perirhizal zone + in this simplification the mode of soluteuptake (steady state, steady rate with no influx, other steady rate) is prescribed by the water uptake + uses a look up tables if present (see create_lookup, and open_lookup) #TODO: add links to the exact lookup table Phi_root matrix flux potential at the root-soil-interface [cm2/d] Phi_soil matrix flux potential at the bulk soil [cm2/d] + r_root root radius [cm] + r_prhiz perirhizal radius [cm] c_bulk solute concentration at the bulk soil [mol/cm3] Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] waterflow steady state waterflow (entire root circumference) [cm2/d] sp van Genuchten parameter set + n_approx how many points are used to approximate the perirhizal solute concentration? + n_approx = 1 means only approximation at the perirhizal radius """ assert len(Phi_root) == len(Phi_soil) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" n_segments = len(c_bulk) - rsc = np.zeros(n_segments) - F = np.zeros(n_segments) #F is a helper values - F_tilde_inv = np.zeros(n_segments) - #print("test",Phi_root[0], Phi_soil[0], c_bulk, Vmax, Km, Ds, waterflow) #TODO remove - if self.lookup_table_solutes: - F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, n_segments)] + #the radii at which the solute concentration will be tested. They are given as relative values between r_root and r_prhiz. r=1 means r_prhiz. + if n_approx ==1: + test_radii = [r_prhiz] + weights = [1] else: - F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, n_segments)] + [x,weights] = ts_roots(n_approx) # returns the nodes and weights for the chebyshev numterical integration + + + rho = [r_prhiz[i]/r_root[i] for i in range(n_segments)] + c_sol_mean2root = np.zeros(n_segments) # ratio of the mean solute concentration to solute concentration next to the root + c_sol_prhiz2root = np.zeros(n_segments) + mean_watercontent = np.zeros(n_segments) #watercontent next to the root + rsc = np.zeros(n_segments) #solute concentration next to the root (output) + F = np.zeros(n_segments) #F is a helper values + F0 = np.zeros(n_segments) #helper value for the ratio of solute next to the root #compute a prefactor D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) + #root watercontent + #root_watercontent = [vg.water_content(vg.fast_imfp[self.sp](Phi_root[i]),self.sp) for i in range(n_segments)] + + #determine mfp depending on r + Phi_A = np.zeros(n_segments) + Phi_C = np.zeros(n_segments) + for i in range(n_segments): + Phi_A[i], Phi_C[i] = self.determine_mfp_function(Phi_root[i], Phi_soil[i], rho[i]) #Phi(r/r_prhiz)= A(s^2-ln(s^2))+C + + + + #determine F0 + if self.lookup_table_solutes: + F0=[self.lookup_table_solutes((Phi_root[i],0)) for i in range(n_segments)] + else: + F0=[self.integral_overDiffusion_(Phi_root[i],self.sp) for i in range(n_segments)] + + for j in range(n_approx): + test_radii = np.array([math.sqrt(x[j] * (r_prhiz[i]**2-r_root[i]**2)+r_root[i]**2) for i in range(n_segments)]) #scale according to volume + s = [test_radii[i]/r_prhiz[i] for i in range(n_segments)] + Phi_current = [Phi_A[i]* (s[i]**2-2*np.log(s[i])) + Phi_C[i] for i in range(n_segments)] + current_watercontent = [vg.water_content(vg.fast_imfp[self.sp](Phi_current[i]),self.sp) for i in range(n_segments)] + if self.lookup_table_solutes: + F=[(self.lookup_table_solutes((Phi_current[i],0))-F0[i]) for i in range(n_segments)] + else: + F=[(self.integral_overDiffusion_(Phi_current[i],self.sp)-F0[i]) for i in range(n_segments)] + for i in range(n_segments): + c_sol_mean2root[i] += weights[j] * math.exp(D_tilde*F[i]) * current_watercontent[i] + mean_watercontent[i] += weights[j] * current_watercontent[i] + for i in range(n_segments): + c_sol_mean2root[i] = c_sol_mean2root[i] / mean_watercontent[i] + Phi_outer = [Phi_A[i] + Phi_C[i] for i in range(n_segments)] + if self.lookup_table_solutes: + F=[(self.lookup_table_solutes((Phi_outer[i],0))-F0[i]) for i in range(n_segments)] + else: + F=[(self.integral_overDiffusion_(Phi_outer[i],self.sp)-F0[i]) for i in range(n_segments)] + + for i in range(n_segments): + c_sol_prhiz2root[i] = math.exp(D_tilde*F[i]) + + print("c_sol_mean2root", c_sol_mean2root, c_sol_prhiz2root) #solve quadratic eqation # TODO: Link to publication for i in range(n_segments): - #print("Dtilde",D_tilde,"F",F[i]) - F_tilde_inv[i]=math.exp(-D_tilde*F[i]) - a1=c_bulk[i]*F_tilde_inv[i] - a2=(1-F_tilde_inv[i])/(waterflow[i]) + a1=c_bulk[i]/c_sol_mean2root[i]/ + a2=(1-1/c_sol_mean2root[i])/(waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) From 8f3584c1722633ffe31ed3a5af9207d94736b3ef Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Mon, 6 Jul 2026 10:28:09 +0200 Subject: [PATCH 31/41] plot of inflow ff approximation --- .../test_perirhizal.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index 4cf5b057a..ab58c61ba 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -38,8 +38,8 @@ do_computation = False #should the computation be run or take the data from a saved file # general parameters -max_time = 0.1 #d -n_times = 10 # number of time intervals +max_time = 10.0 #d +n_times = 1000 # number of time intervals r_prhiz = 0.6 # perirhizal radius[cm] r_root = 0.02 # root radius [cm] NC = 40 # number of spatial discretisations @@ -155,7 +155,8 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu s.setParameter("Soil.BC.Top.CValue", str(initial_soluteconcentration*molarMassSolute)) s.setParameter("Soil.BC.Bot.SType", "8") # michaelisMenten=8 (SType = Solute Type) s.setParameter("Soil.BC.Bot.CValue", "0.0") #should not matter - s.setParameter("RootSystem.Uptake.Vmax", str(Vmax_per_area*molarMassSolute)) #mol/(cm2d) -> g/(cm2 d) + #s.setParameter("RootSystem.Uptake.Vmax", str(Vmax_per_area*molarMassSolute)) #mol/(cm2d) -> g/(cm2 d) + s.setParameter("RootSystem.Uptake.Vmax", str(Vmax*molarMassSolute)) #mol/d -> g/d #TODO: Vmax or Vmax per area? s.setParameter("RootSystem.Uptake.Km", str(Km*molarMassSolute)) # mol/cm3 -> g/cm3 "RootSystem.Uptake.Vmax" "RootSystem.Uptake.Km" @@ -318,8 +319,9 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #result_solutes_ss_nf = peri.soil_root_solutes_ss_([Phi_root_nf], [Phi_outer_nf], [solutes_nf[-1]], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) #result_solutes_sr_nf = peri.soil_root_solutes_sr_([Phi_root_nf], [Phi_outer_nf], [rho], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) - result_solutes_ss_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp, n_approx = 5) - result_solutes_sr_nf = result_solutes_ss_nf + result_solutes_ss_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [abs(waterdemand)], peri.sp, n_approx = 1) + result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [abs(radial_waterdemand)], peri.sp, n_approx = 1) + #safe the results #(here there is no computed inflow, so index 1 doesn't do anything) result_solutes_ss_nf = result_solutes_ss_nf[0] @@ -337,8 +339,8 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu F_g = peri.integral_overDiffusion_(Phi_g(r_current),peri.sp)-F0_g F_tilde_nf=math.exp(D_tilde*F_nf) F_tilde_g=math.exp(D_tilde*F_g) - solutes_dumux_ss[0,r,2+j] = result_solutes_ss_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_ss[0,r,0] / radial_waterdemand#waterdemand is assumed to be negative #TODO: look at the signs - solutes_dumux_sr[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_sr[0,r,0] / radial_waterdemand #an uptake of both water and solute is assumed + solutes_dumux_ss[0,r,2+j] = result_solutes_ss_nf * F_tilde_nf + (1-F_tilde_nf) * solutes_dumux_ss[0,r,0] / waterdemand#waterdemand is assumed to be negative #TODO: look at the signs + solutes_dumux_sr[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf + (1-F_tilde_nf) * solutes_dumux_sr[0,r,0] / radial_waterdemand #an uptake of both water and solute is assumed #case of general steady rate water uptake #for the steady state take again the outer concentration From bab7942d7f6b950a76dc52846ede69b8d677bf37 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Tue, 7 Jul 2026 15:42:55 +0200 Subject: [PATCH 32/41] debugging: mean works --- .../test_perirhizal.py | 56 +-- src/functional/Perirhizal.py | 455 +++++++----------- 2 files changed, 189 insertions(+), 322 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index ab58c61ba..f42d9134d 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -35,11 +35,11 @@ # run the dumux implementation of root water and nitrate uptake, later compare it to the analytical approximation n_tests = 1 #try everything here for this many random parameter sets -do_computation = False #should the computation be run or take the data from a saved file +do_computation = True #should the computation be run or take the data from a saved file # general parameters -max_time = 10.0 #d -n_times = 1000 # number of time intervals +max_time = 0.1 # d +n_times = 10 # number of time intervals r_prhiz = 0.6 # perirhizal radius[cm] r_root = 0.02 # root radius [cm] NC = 40 # number of spatial discretisations @@ -287,12 +287,12 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #no flux outer BC Phi_soil_nf = vg.fast_mfp[peri.sp](mean_waterpotential_nf) #mfp of the mean soil Phi_outer_nf = Phi_soil_nf - (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_nf*r_prhiz)*r_prhiz*np.log(0.53) #mfp at the outer perirhizal radius - Phi_nf = lambda r: Phi_soil_nf + (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_nf*r_prhiz)*r_prhiz*np.log(r/r_prhiz)#mfp function depending on radius + Phi_nf = lambda r: Phi_outer_nf + (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_nf*r_prhiz)*r_prhiz*np.log(r/r_prhiz)#mfp function depending on radius Phi_root_nf = Phi_nf(r_root)#mfp next to the root #general outer BC: Dirichlet BC to a fixed potential Phi_soil_g = vg.fast_mfp[peri.sp](mean_waterpotential_g) #mfp of the mean soil Phi_outer_g = Phi_soil_g - (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_g*r_prhiz)*r_prhiz*np.log(0.53) #mfp at the outer perirhizal radius - Phi_g = lambda r: Phi_soil_g + (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_g*r_prhiz)*r_prhiz*np.log(r/r_prhiz)#mfp function depending on radius + Phi_g = lambda r: Phi_outer_g + (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_g*r_prhiz)*r_prhiz*np.log(r/r_prhiz)#mfp function depending on radius Phi_root_g = Phi_nf(r_root)#mfp next to the root #write the steady rate approximations for the water as outputs @@ -319,7 +319,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #result_solutes_ss_nf = peri.soil_root_solutes_ss_([Phi_root_nf], [Phi_outer_nf], [solutes_nf[-1]], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) #result_solutes_sr_nf = peri.soil_root_solutes_sr_([Phi_root_nf], [Phi_outer_nf], [rho], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) - result_solutes_ss_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [abs(waterdemand)], peri.sp, n_approx = 1) + result_solutes_ss_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [abs(waterdemand)], peri.sp, n_approx = 5) result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [abs(radial_waterdemand)], peri.sp, n_approx = 1) #safe the results @@ -329,14 +329,14 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu result_solutes_sr_nf = result_solutes_sr_nf[0] solutes_dumux_sr[0,r,0]=-Vmax_per_area * result_solutes_sr_nf / (Km + result_solutes_sr_nf) - F0_nf = peri.integral_overDiffusion_(Phi_root_nf,peri.sp) - F0_g = peri.integral_overDiffusion_(Phi_root_g,peri.sp) + F0_nf = peri.integral_AdvectionDiffusion_(Phi_root_nf,peri.sp) + F0_g = peri.integral_AdvectionDiffusion_(Phi_root_g,peri.sp) D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) for j in range(NC): r_current = CC[j] - F_nf = peri.integral_overDiffusion_(Phi_nf(r_current),peri.sp)-F0_nf - F_g = peri.integral_overDiffusion_(Phi_g(r_current),peri.sp)-F0_g + F_nf = peri.integral_AdvectionDiffusion_(Phi_nf(r_current),peri.sp)-F0_nf + F_g = peri.integral_AdvectionDiffusion_(Phi_g(r_current),peri.sp)-F0_g F_tilde_nf=math.exp(D_tilde*F_nf) F_tilde_g=math.exp(D_tilde*F_g) solutes_dumux_ss[0,r,2+j] = result_solutes_ss_nf * F_tilde_nf + (1-F_tilde_nf) * solutes_dumux_ss[0,r,0] / waterdemand#waterdemand is assumed to be negative #TODO: look at the signs @@ -345,26 +345,26 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #case of general steady rate water uptake #for the steady state take again the outer concentration #TODO: check weather Vmax or Vmax per area - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr_ss") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_soil_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ss") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) solutes_dumux_ss[1,r,0] = -Uptake[0] solutes_dumux_ss[1,r,1] = -ss_uptake[0] solutes_dumux_ss[1,r,2:] = soluteconcentration[:] solutes_dumux_ss[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate no flux outer BC - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [-inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr_nf") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) - solutes_dumux_sr[1,r,0] = -Uptake[0] - solutes_dumux_sr[1,r,1] = -ss_uptake[0] - solutes_dumux_sr[1,r,2:] = soluteconcentration[:] - solutes_dumux_sr[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area + #rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_soil_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr") + #_, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + #solutes_dumux_sr[1,r,0] = -Uptake[0] + #solutes_dumux_sr[1,r,1] = -ss_uptake[0] + #solutes_dumux_sr[1,r,2:] = soluteconcentration[:] + #solutes_dumux_sr[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate solute uptake with the farfield approximation - rsc, Uptake, ss_uptake, sr_uptake = peri.soil_root_solutes_new([Phi_soil_g], [rootuptake_w_g*r_root], [-inflow_w_g*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr_ff") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_soil_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) - solutes_dumux_ff[1,r,0] = -Uptake[0] - solutes_dumux_ff[1,r,1] = -ss_uptake[0] - solutes_dumux_ff[1,r,2:] = soluteconcentration[:] - solutes_dumux_ff[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area + #rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_soil_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ff") + #_, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + #solutes_dumux_ff[1,r,0] = -Uptake[0] + #solutes_dumux_ff[1,r,1] = -ss_uptake[0] + #solutes_dumux_ff[1,r,2:] = soluteconcentration[:] + #solutes_dumux_ff[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area return watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_dumux_sr, solutes_dumux_ss, solutes_dumux_ff @@ -374,14 +374,6 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu # save everything in the np arrays for i in range(n_tests): watercontent_dumux[i,:,:,:], waterpotential_dumux[i,:,:,:], watercontent_sr[i,:,:,:], waterpotential_sr[i,:,:,:], solutes_dumux[i,:,:,:], solutes_dumux_sr[i,:,:,:], solutes_dumux_ss[i,:,:,:], solutes_dumux_ff[i,:,:,:] = run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volumes, length, n_scenarios, initial_waterpotential, initial_soluteconcentration) - #watercontent_dumux[i,:,:,:] = watercontent_dumux[:,:,:] - #waterpotential_dumux[i,:,:,:] = waterpotential_dumux[:,:,:] - #watercontent_sr[i,:,:,:] = watercontent_sr[:,:,:] - #waterpotential_sr[i,:,:,:] = waterpotential_sr[:,:,:] - #solutes_dumux[i,:,:,:] = solutes_dumux[:,:,:] - #solutes_dumux_sr[i,:,:,:] = solutes_dumux_sr[:,:,:] - #solutes_dumux_ss[i,:,:,:] = solutes_dumux_ss[:,:,:] - #solutes_dumux_ff[i,:,:,:] = solutes_dumux_ff[:,:,:] np.savez("test_perirhizal.npz", watercontent_dumux=watercontent_dumux, diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 9948045da..e7e5dd4db 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -44,12 +44,13 @@ def __init__(self, ms=None): self.lookup_table = None # optional 2d look up table to find soil root interface potentials self.global_lookup_table = None # optional 3d look up table to find soil root interface potentials - self.lookup_table_solutes = None # optional 1d look up table for steady state solute flow - self.lookup_table_sr_solutes = None # optional 2d lookup tables for the steady rate solute flow (one every diffusion coefficient) + self.lookup_table_sr_solutes = None # optional 1d look up table for steady state solute flow + self.lookup_table_sr_solutes_simplified = None # optional 2d lookup tables for the steady rate solute flow (one every diffusion coefficient) self.sp = None # corresponding van genuchten soil parameter self.alpha_0 = 0.3 # a constant that is used for numerically solving the perirhizal waterflow self.h_wilt = -16000 + self.Ds0_ref = 1 # reference diffusion coefficient of solutes in water [cm2/d] self.water_filename = "lookup_perirhizal_waterflow_global" #name and location for the global lookup table def set_soil(self, sp): @@ -58,8 +59,8 @@ def set_soil(self, sp): self.sp = sp self.lookup_table = None self.global_lookup_table = None - self.lookup_table_solutes = None self.lookup_table_sr_solutes = None + self.lookup_table_sr_solutes_simplified = None def open_lookup(self, filename): @@ -81,24 +82,37 @@ def open_global_lookup(self, filename): self.lookup_global_mfp = RegularGridInterpolator((vg_m_, sx_), interface_vg) #.sp = vg.Parameters(soil) - def open_lookup_solutes(self, filename): + def open_lookup_solutes_simplified(self, filename): """opens a look-up table from a file to quickly find soil root solute concentrations in the steady state case""" npzfile = np.load(filename + ".npz") - integral_overD_ = npzfile["integral_overD_"] + integral_AdvDiff_ = npzfile["integral_AdvDiff_"] base_mfp_ = npzfile["base_mfp_"] soil = npzfile["soil"] - self.lookup_table_solutes = RegularGridInterpolator((base_mfp_,[0,1]) , integral_overD_) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_table_sr_solutes_simplified = RegularGridInterpolator((base_mfp_,[0,1]) , integral_AdvDiff_) # method = "nearest" fill_value = None , bounds_error=False self.sp = vg.Parameters(soil) vg.create_mfp_lookup(self.sp) # does this have to be repeated here? - def open_lookup_sr_solutes(self, filename): + def open_lookup_solutes(self, filename): """opens an additional look-up table from a file to quickly find soil root solute concentrations in the steady rate case""" npzfile_sr = np.load(filename + ".npz") - self.open_lookup_solutes(filename) #repeat steady state case - R_sr_ = npzfile_sr["R_sr_"] - Ds_, outer_mfp_, inner_mfp_ = npzfile_sr["Ds_"], npzfile_sr["outer_mfp_"], npzfile_sr["inner_mfp_"] + Ds_, outer_mfp_, inner_mfp_ = npzfile_sr["Phi1_"], npzfile_sr["waterflow_"], npzfile_sr["wateruptake_"], npzfile_sr["r_eval_"] soil = npzfile_sr["soil"] - self.lookup_table_sr_solutes = RegularGridInterpolator((Ds_, inner_mfp_, outer_mfp_) , R_sr_) # method = "nearest" fill_value = None , bounds_error=False + conc_rel_c, inflow_rel_c, Uptake_rel_c = npzfile_sr["conc_rel_c"], npzfile_sr["inflow_rel_c"], npzfile_sr["Uptake_rel_c"] + conc_mean_c, inflow_mean_c, Uptake_mean_c = npzfile_sr["conc_mean_c"], npzfile_sr["inflow_mean_c"], npzfile_sr["Uptake_mean_c"] + self.lookup_table_sr_solutes = { + "conc_rel_c" : [0], + "conc_mean_c" : [0], + "inflow_rel_c" : [0], + "inflow_mean_c" : [0], + "Uptake_rel_c" : [0], + "Uptake_mean_c" : [0] + } + self.lookup_table_sr_solutes["conc_rel_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , conc_rel_c) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_table_sr_solutes["conc_mean_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , conc_mean_c) + self.lookup_table_sr_solutes["inflow_rel_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , inflow_rel_c) + self.lookup_table_sr_solutes["inflow_mean_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , inflow_mean_c) + self.lookup_table_sr_solutes["Uptake_rel_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , Uptake_rel_c) + self.lookup_table_sr_solutes["Uptake_mean_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , Uptake_mean_c) self.sp = vg.Parameters(soil) vg.create_mfp_lookup(self.sp) # does this have to be repeated here? @@ -251,8 +265,6 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, lamb = segLength*r_root[i]*Vmax[i]/(D*Km[i]) epsilon = segLength*r_root[i]*E[i]/(D*Km[i]) - print("Tiinatest", t[i], c_inf,epsilon,l_func(t[i]),Pe,lamb,Pe) - #compute the unitless uptake if Pe<=(lamb/(1+c_inf)): F[i] = 2*lamb*(c_inf+epsilon*l_func(t[i])) / (1+c_inf+l_func(t[i])*(lamb + epsilon) +math.sqrt(4*(c_inf+epsilon * l_func(t[i]))+(1-c_inf+(lamb-epsilon)*l_func(t[i]))**2)) #Eqn. [9] @@ -270,9 +282,9 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, return rsc - def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km, Ds, sp, mode = "sr_ff"): + def soil_root_solutes_sr(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km, Ds, sp, mode = "ff"): """ - steady state assumption of solute uptake by roots TODO: insert citaiton + steady rate assumption of solute uptake by roots TODO: insert citaiton Phi_soil outer matrix flux potential [cm2/d] rootwateruptake radial root water uptake [cm2/d] @@ -285,54 +297,50 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] sp van Genuchten parameter set - mode what model is used? pure ss, sr with no flux or sr with the far field approximation + mode what mode is used for the solute uptake? + "ss" for steady state solute uptake + "sr" steady rate solute uptake with a no flux outer BC + "ff" for steady rate uptake in combination with the far field approximation to give an outer Cauchy BC #TODO: cite paper for far field approximation output: rsc solute concentration next to the root [mol/cm3] Uptake solute uptake of the root [mol/(cm d)] """ - assert len(Phi_soil) == len(rootwateruptake) == len(waterinflow) == len(r_root) == len(r_prhiz) == len(c_soil) == len(c_outer) == len(Vmax) == len(Km) == len(Ds), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" + assert len(Phi_soil) == len(rootwateruptake) == len(waterinflow) == len(r_root) == len(r_prhiz) == len(c_soil) == len(c_outer) == len(Vmax) == len(Km), "Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax and Km must have the same length" n_segments = len(c_soil) - rsc = np.zeros(n_segments) - Uptake = np.zeros(n_segments) - ss_uptake = np.zeros(n_segments) - sr_uptake = np.zeros(n_segments) + rsc = np.zeros(n_segments) #conentration next to the root + Uptake = np.zeros(n_segments) #total solute uptake of the root + ss_uptake = np.zeros(n_segments) #inflow into the parirhizal zone + sr_uptake = np.zeros(n_segments) #solute depletion rate of the perirhizal zone #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): rho = r_prhiz[i] / r_root[i] - #print("rootwateruptake",rootwateruptake[i],waterinflow[i]) Phi_out = Phi_soil[i] -( (rootwateruptake[i]-waterinflow[i])*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow[i]*np.log(0.53) ) - #Phi_out = root_scalar(Phi_diff, method="brentq", bracket=[1e-5, Phi_soil]).root - Phi = lambda r_rel : Phi_out + (rootwateruptake[i]-waterinflow[i])*((r_rel**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) #TODO: test using Phi_soil instead of Phi_outer - radial_waterflow = lambda r_rel : waterinflow[i] + (rootwateruptake[i]-waterinflow[i]) * (1 - r_rel**2) / (1-1/rho**2) + #Phi = lambda r_rel : Phi_out + (rootwateruptake[i]-waterinflow[i])*((r_rel**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) #TODO: test using Phi_soil instead of Phi_outer + Phi = lambda r : Phi_out + (rootwateruptake[i] - waterinflow[i]) * ((r/r_prhiz[i])**2/2-1/2+np.log(r_prhiz[i]/r)) * rho**2 / (1 - rho**2) + waterinflow[i] * np.log(r / r_prhiz[i]) + Phi = lambda r : 1.0 #TODO: remove times 3 + radial_waterflow = lambda r : (waterinflow[i] + (rootwateruptake[i]-waterinflow[i]) * (r_prhiz[i]**2 - r**2) / (r_prhiz[i]**2 - r_root[i]**2)) / (2 * np.pi * r) Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel)),sp),10/3) / (sp.theta_S**2) # Millington and Quirk - #values for the subfunction - #print("Phi", Phi(1e-10), Phi(r_prhiz[i])) - # = root_scalar(Phi, method="brentq", bracket=[1e-10, r_prhiz[i]]).root - - r_crit = r_root[i] / 100 - - Ds0 = Ds[i] - wateruptake_rel = (rootwateruptake[i] - waterinflow[i]) / ((r_prhiz[i]**2 - r_root[i]**2) * np.pi) - critical_wateruptake = rootwateruptake[i] + wateruptake_rel * ((r_root[i]**2 - r_crit**2) * np.pi) - r_eval = [r_crit, r_root[i], (r_root[i]+r_prhiz[i])/2, r_prhiz[i]] + Ds0 = self.Ds0_ref + scaling = math.sqrt(Ds[i] / Ds0) #I am an idiot I am an idiot. I messed up the ordering + Phi1 = Phi(1/scaling) - waterinflow[i] * np.log(scaling) - (rootwateruptake[i] - waterinflow[i]) * r_prhiz[i]**2 / (r_prhiz[i]**2 - r_root[i]**2) * (np.log(scaling)-1/2 * (1-1/scaling**2)) + waterflow = radial_waterflow(scaling) / scaling + wateruptake = (rootwateruptake[i] - waterinflow[i]) * r_prhiz[i]**2 / (r_prhiz[i]**2 - r_root[i]**2) / scaling**2 - #use the subfunction #TODO: alternative lookup table - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) + r_eval = np.logspace(np.log10(r_root[i]), np.log10(r_prhiz[i]), num = 40) + r_eval = [r * scaling for r in r_eval] + Phi1 = 1 + waterflow = 0 + wateruptake = 0 + print("r_eval equation before",r_root[i], r_prhiz[i],r_eval) + conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi1, waterflow, wateruptake, r_eval, sp) - #fix discrepancy between critical and root radius - #TODO: turn the means into floats instead of lists with one entry - #conc_rel_c = conc_rel_c[1:]#/conc_rel_c[1] - #conc_mean_c = (conc_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-conc_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) - #inflow_rel_c = inflow_rel_c[1:]#/inflow_rel_c[1] - #inflow_mean_c = (inflow_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-inflow_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) - #Uptake_rel_c = Uptake_rel_c[1:]#/Uptake_rel_c[1] - #Uptake_mean_c = (Uptake_mean_c[1:]*(r_prhiz[i]**2-r_crit**2)-Uptake_mean_c[1]*(r_root[i]**2-r_crit*2))/(r_prhiz[i]**2-r_root[i]**2) + print("r_eval equation",r_root[i], r_prhiz[i],r_eval) #default prefactors for the steady state case pre_c = 0 @@ -344,40 +352,31 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, match mode: case "ss": #steady state pre_c, pre_srUptake, pre_inflow, absolute = 0, 1, 0, 0 - case "sr_nf": #steady rate uptake, no influx - pre_c, pre_srUptake, pre_inflow, absolute = 0, 0, 1, 0 - case "sr_ff": #far field approximation # TODO insert link to Tiina Roose publication - #E_1(x)=int(x,inf) exp(-y)/y dy = -exp(x) - int(x,inf) -exp(-y) -exp( - #c(r_prhiz) = c_inf + B*E(r**2) - #inflow = Ds * B * exp(-r_prhiz)/r_prhiz + waterflow * c(r_prhiz) - #c_inf = outer c ? - # outer c = c_inf + B*mean(E)(2*r**2,r**2) - - # B = (outer_c - c(r_prhiz)) / (mean(E)-E) - # mean(E)(2*r**2,r**2)=E(r**2)+int(E(x)-E(r**2),r**2,2*r**2) / r**2=E(r**2)-int(r**2,2*r**2,y*exp(-y)/y) / r**2=E(r**2)+(exp(-2*r**2)-exp(r**2)) / r**2 - # B = (outer_c - c(r_prhiz)) * (r_prhiz**2) / (exp(-2*r**2)-exp(-r**2)) - # this means inflow can be computed + case "sr": #steady rate uptake, no influx + pre_c, pre_srUptake, pre_inflow, absolute = 0, -1, 1, 0 + case "ff": #far field approximation # TODO insert link to Tiina Roose publication, and current application B_abs = c_outer[i] * (r_prhiz[i]**2)/ (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) B_cprhiz = -1 * (r_prhiz[i]**2) / (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) pre_inflow = 1 - pre_c = - Ds_func(r_prhiz[i]) * B_cprhiz * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 - waterinflow[i] + pre_c = - Ds_func(r_prhiz[i]) * B_cprhiz * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 - waterinflow[i] / (2 * np.pi * r_prhiz[i]) #TODO check sign absolute = Ds_func(r_prhiz[i]) * B_abs * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 - pre_srUptake = 0 + #pre_c = - Ds_func(r_prhiz[i]) * B_cprhiz * math.exp(-r_prhiz[i]) / r_prhiz[i] - waterinflow[i] / (2 * np.pi * r_prhiz[i]) #TODO check sign + #absolute = Ds_func(r_prhiz[i]) * B_abs * math.exp(-r_prhiz[i]) / r_prhiz[i] + pre_srUptake = 1 + case "dirichlet": + pre_c = 1 + absolute = c_outer[i] case _: #default pre_c, pre_srUptake, pre_inflow, absolute = 0, 1, 0, 0 - #equation - #c11 * ss flow + c12 * sr uptake + c13 * rsc = c_mean - #c21 * ss flow + c22 * sr uptake + c23 * rsc = c_prhiz - #ss flow + sr uptake = Uptake = f(rsc) - #pre_c * c(r_prhiz) + pre_srUptake * srUptake + pre_inflow * inflow (= ssflow) = absolute - #pre_c * (c21 * ss flow + c22 * sr uptake + c23 * rsc) + pre_srUptake * srUptake + pre_inflow * inflow = absolute #variables to eliminate: ss flow, sr uptake, c_prhiz, Uptake # variable for the final equations: rsc A = np.zeros((4,4)) b = np.zeros((4,2)) #right hand side of the linear equation, one for absolute values, one for the rsc value + #linear equations for c_prhiz, rsc, ss_flow, sr_uptake, Uptake (4 linear, 1 quadratic) + #c11 * ss flow + c12 * sr uptake + c13 * rsc = c_mean A[0,0] = inflow_mean_c[-1] A[0,1] = Uptake_mean_c[-1] @@ -420,33 +419,24 @@ def soil_root_solutes_new(self, Phi_soil, rootwateruptake, waterinflow, r_root, C = Km[i]*n[3] p = B/A q = C/A - if p>0: - rsc[i] = - p/2 + math.sqrt(p**2/4-q) #only the positive solution makes sense #test for negative + if p<0: + rsc[i] = - p/2 - math.sqrt(p**2/4-q) else: - rsc[i] = - p/2 + math.sqrt(p**2/4-q) #this is the standard case as m[3] will usually be negative - # TODO: twice the same sign in the lines before + rsc[i] = - p/2 + math.sqrt(p**2/4-q) Uptake[i] = m[3]*rsc[i]+n[3] ss_uptake[i] = m[0]*rsc[i]+n[0] sr_uptake[i] = m[1]*rsc[i]+n[1] - #1 more equation necessary, multiple possibilities: - # only ss: sr uptake = 0, but how? maybe just advection? - # only sr: ss flow = 0, set the solute inflow to advection? - # combination: set the inflow to a preset number based on either another model (Tiina) or diffusion? - # best: cite Tiina that mostly Diffusion is important (on the outside of the interval?) and then compute the inflow through a resistance + Advection (both take into account the outer concentration and diffusion coefficient - # far field solution c = c_inf - B*E_1, E_1(x)=\int_1înf exp(-y)/y dy -use this to determine the outsides concentration gradient - # c_inf is the outer concentration, for a given c(r_prhiz) this then determines the solute inflow (thrrough the gradient - - # similarities? sr and combination both give the inflow rate depending on c(r_prhiz) (sr has 0), ss gives a linear combination of sr-Uptake - # so this can be generalised through c(r_prhiz), sr-Uptake and inflow (linear equation) - #linear equations for c_prhiz, rsc, ss_flow, sr_uptake, Uptake (4 linear, 1 quadratic) - return rsc, Uptake, ss_uptake, sr_uptake + print("output", conc_mean_c[-1], inflow_mean_c[-1], Uptake_mean_c[-1], c_soil) + print("linear", rsc*conc_mean_c[-1]+ss_uptake* inflow_mean_c[-1]+sr_uptake* Uptake_mean_c[-1], c_soil) + + return rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c - def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, r_eval, c_root, Ds0, ss_uptake, sr_uptake, sp): + def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prhiz, r_eval, c_root, Ds0, ss_uptake, sr_uptake, sp): """ given the water and solute uptake data this computes the discretisation of the steady rate solutions - Phi_soil outer matrix flux potential [cm2/d] + Phi_out outer matrix flux potential [cm2/d] rootwateruptake radial root water uptake [cm2/d] waterinflow radial water inflow at r_prhiz [cm2/d] r_root root radius [cm] @@ -462,53 +452,67 @@ def watersolutes_disc(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_pr watercontent, waterpotential, soluteconcentration discretisations """ - - #simple computaitons + #simple computations rho = r_prhiz / r_root - #reference radius for the solute implementation is either at the perirhizal radius (steady state water uptake) or at the radius without wateruptake - #in both cases it is scaled by Ds0 / self.Ds0 - #r_rel0 = r_prhiz * Ds0 / self.Ds0 - - #water is easy - Phi_out = Phi_soil -( (rootwateruptake-waterinflow)*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow*np.log(0.53) ) - Phi = lambda r : Phi_out + (rootwateruptake - waterinflow) * ((r/r_root)**2/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(r_prhiz/r)-0.5)) + waterinflow * np.log(r / r_prhiz) - Phi = lambda r : Phi_soil #TODO: remove test + + #water + #Phi_out = Phi_soil -( (rootwateruptake-waterinflow)*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow*np.log(0.53) ) + Phi = lambda r : Phi_out - (rootwateruptake - waterinflow) * ((r/r_prhiz)**2/2-1/2+np.log(r_prhiz/r)) * r_prhiz**2 / (r_prhiz**2 - r_root**2) + waterinflow * np.log(r / r_prhiz) + Phi = lambda r : 1.0 #TODO: remove times 3 + wateruptake = (rootwateruptake - waterinflow) / (np.pi * (r_prhiz**2 - r_root**2))#TODO doublecheck + radial_waterflow = lambda r : (waterinflow + wateruptake * np.pi * (r_prhiz**2 - r**2)) / (2 * np.pi * r) #TODO: rename waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) - #print("Phi_root",Phi(1e-10), Phi(r_prhiz)) - #r_crit = root_scalar(Phi, method="brentq", bracket=[1e-10,r_prhiz]).root - r_crit = r_root / 100 + #solutes - #r_eval = [r_crit, r_eval] - r_eval = np.concatenate(([r_crit],r_eval)) #use the subfunction #TODO: alternative lookup table - wateruptake_rel = (rootwateruptake - waterinflow) / ((r_prhiz**2 - r_root**2) * np.pi) - critical_wateruptake = rootwateruptake + wateruptake_rel * (r_root**2 - r_crit**2) * np.pi - - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp) - - soluteconcentration = c_root * conc_rel_c + ss_uptake * inflow_rel_c + sr_uptake * Uptake_rel_c + scaling = math.sqrt(Ds0 / self.Ds0_ref) + Phi1 = Phi(1/scaling) - waterinflow * np.log(scaling) - (rootwateruptake - waterinflow) * r_prhiz**2 / (r_prhiz**2 - r_root**2) * (np.log(scaling)-1/2 * (1-1/scaling**2)) + waterflow = radial_waterflow(scaling) / scaling + r_eval = [r * scaling for r in r_eval] + wateruptake = (rootwateruptake - waterinflow) * r_prhiz**2 / (r_prhiz**2 - r_root**2) / scaling**2 + + Phi1 = 1 + waterflow = 0 + wateruptake = 0 + + Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d + Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) + Km = 1.5e-7 #mol/cm3 + + #rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([Phi_out], [rootwateruptake], [waterinflow], [r_root], [r_prhiz], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") + rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([1], [0], [0], [r_root], [r_prhiz], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") + #conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi1, waterflow, wateruptake, r_eval, sp) + conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(1, 0, 0, r_eval, sp) + c_root = rsc + print("compareinflow", inflow_mean_c_0, inflow_mean_c) + print("r_eval disc", r_eval) + + soluteconcentration = c_root * conc_rel_c + ss_uptake * inflow_rel_c + sr_uptake * Uptake_rel_c + soluteconcentration_mean = c_root * conc_mean_c + ss_uptake * inflow_mean_c + sr_uptake * Uptake_mean_c + + print("output disc1", c_root, ss_uptake, sr_uptake, scaling) + print("output disc2", conc_rel_c[0], inflow_rel_c[0], Uptake_rel_c[0], soluteconcentration[0]) + print("output disc3", conc_rel_c[-1], inflow_rel_c[-1], Uptake_rel_c[-1], soluteconcentration[-1]) + print("output disc4", conc_mean_c[-1], inflow_mean_c[-1], Uptake_mean_c[-1], soluteconcentration_mean[-1]) watercontent = np.array([watercontent_func(r) for r in r_eval]) waterpotential = np.array([waterpotential_func(r) for r in r_eval]) return watercontent, waterpotential, soluteconcentration - def solute_linearequation_sr_(self, Ds0, critical_wateruptake, wateruptake_rel, r_eval, sp): + def solute_linearequation_sr_(self, Phi1, waterflow, wateruptake, r_eval, sp): """ computes a linear equation from the diffusion advection ODE on the perirhizal solute flow #TODO: insert equation number + the radius (and steady rate wateruptake) are scaled by the diffusion coefficient of the solute - r_rel[0] corresponds to the critical radius at which the permanent wilting point is reached (Phi = 0) - r_rel = 1 corresponds to the perirhizal radius - - Ds0 diffusion coefficient of the solute in water [cm2/d] - critical_wateruptake the theoretical radial wateruptake at the radius for which Phi = 0[cm2/d] - wateruptake_rel wateruptake in each volume, both wateruptake and volume are scaled according to r_rel [cm3/(cm3d)] - r_eval (1d array) the equation is solved from r_rel[0] to r_eval [-1]. + Phi1 matrix flux potential at \tilde{r} = 1 [cm2/d] + waterflow waterflow at r = 1 [cm/d] + wateruptake steady rate wateruptake throughout the perirhizal zone, scaled to \tilde{r} [cm3/(cm3d)] + r_eval the equation is solved from r_eval[0] to r_eval[-1], it is a 1d array [cm] outputs: - conc_rel_c ratio of c(r_rel[i]) to c(r_rel[0]) for d_r c(1)=0 conc_mean_c mean solute concentrations for the steady state solute uptake inflow_rel_c ratio of c(r_rel[i]) to d_r c(1) for c(1)=0 @@ -517,10 +521,10 @@ def solute_linearequation_sr_(self, Ds0, critical_wateruptake, wateruptake_rel, Uptake_mean_c mean solute concentrations relative to rel_soluteUptake """ - #TODO:formulate this for ln r so as to make it more robust against small critical radii + #reference diffusion coefficient to which r was scaled to [cm2/d] + Ds0 = self.Ds0_ref - r_crit = r_eval[0] - r_eval = r_eval[1:] + #r_root = r_eval[0] n = len(r_eval) conc_rel_c = np.ones(n) @@ -534,55 +538,34 @@ def solute_linearequation_sr_(self, Ds0, critical_wateruptake, wateruptake_rel, watercontent_times_r = np.ones(n) #waterflow - Phi = lambda r_rel : wateruptake_rel*(r_rel**2/2 - r_crit**2/2 - np.log(r_rel / r_crit)) + (critical_wateruptake - wateruptake_rel *(1-r_crit**2)*np.pi)*np.log(r_rel)#[cm2/d] - Phi = lambda r_rel : vg.fast_imfp[sp](-200) - radial_waterflow = lambda r_rel : - (critical_wateruptake - wateruptake_rel*(r_rel**2 - r_crit**2)*np.pi) - watercontent = lambda r_rel : vg.water_content(vg.fast_imfp[sp](Phi(r_rel)), sp) - Ds = lambda r_rel : Ds0 * math.pow(watercontent(r_rel),10/3) / (sp.theta_S**2) + Phi = lambda r : Phi1 - wateruptake * (r**2/2 - 1/2 - np.log(r)) + waterflow * 2 * np.pi * 1 * np.log(r) # TODO add citation + Phi = lambda r : 1.0 #TODO: remove times 3 + radial_waterflow = lambda r : (2 * np.pi * 1 * waterflow + wateruptake * np.pi * (1 - r**2)) #TODO: distinct waterflow [cm/d] and radial waterflow [cm2/d] + watercontent = lambda r : vg.water_content(vg.fast_imfp[sp](Phi(r)), sp) + Ds = lambda r : Ds0 * math.pow(watercontent(r),10/3) / (sp.theta_S**2) - f_homogen = lambda c, r_rel : ( radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference - f_steadystate = lambda c, r_rel : (1 + radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference - f_steadyrate = lambda c, r_rel : ((1**2-r_rel**2)*np.pi + radial_waterflow(r_rel) * c) / (Ds(r_rel) * r_rel) #TODO: add reference + f_homogen = lambda c, r : ( radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference + f_steadystate = lambda c, r : (1 + radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference + f_quadratic = lambda c, r : (r**2 + radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference #numerically solve the ODE c' = f(r_rel,c) starting at r_rel=1 conc_rel_c = odeint(f_homogen, y0 = 1, t = r_eval) conc_rel_c = np.array([element[0] for element in conc_rel_c]) inflow_rel_c = odeint(f_steadystate, y0 = 0, t = r_eval) inflow_rel_c = np.array([element[0] for element in inflow_rel_c]) - Uptake_rel_c = odeint(f_steadyrate, y0 = 0, t = r_eval) + Uptake_rel_c = odeint(f_quadratic, y0 = 0, t = r_eval) Uptake_rel_c = np.array([element[0] for element in Uptake_rel_c]) #compute the means - watercontent_times_r = np.array([watercontent(r_rel)*r_rel for r_rel in r_eval]) - conc_mean_c = np.array([np.average(conc_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) - inflow_mean_c = np.array([np.average(inflow_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) - Uptake_mean_c = np.array([np.average(Uptake_rel_c[0:j], weights=watercontent_times_r[0:j]) for j in range(1,n)]) - print("n conc_mean_c",n,len(conc_mean_c), conc_mean_c) - return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c - - def soil_root_solutes_combined(self, Phi_soil, rootwateruptake, soilwateruptake, c_prhiz, c_bulk, Vmax, Km, Ds, waterflow, sp): - """ - combines a steady state assumption of solutes next to the roots with the far field approximation by Tiina Roose TODO: insert citaitons - In the perirhizal zone it is a steady rate uptake with defined mean concentration. - The relation of c(r_prhiz) and d_r c(r_prhiz) is given by Roose - - Phi_out outer matrix flux potential [cm2/d] - rootwateruptake radial root water uptake [cm2/d] - soilwateruptake radial water inflow [cm2/d] - c_soil mean solute concentration in the cylinder [mol/cm3] - Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] - Km half saturation constant Michaelis Menten Kinetics [mol/cm3] - Ds Diffusion constant in water [cm2/d] - waterflow steady state waterflow (entire root circumference) [cm2/d] - sp van Genuchten parameter set - output_disc should the discretisation be put out - - output: - c_root solute concnetration next to the root [mol/cm3] - disc discretisation of the solute computation [mol/cm3] + conc_mean_c[0] = conc_rel_c[0] + inflow_mean_c[0] = inflow_rel_c[0] + Uptake_mean_c[0] = Uptake_rel_c[0] + watercontent_times_r = np.array([watercontent(r)*r for r in r_eval]) + conc_mean_c[1:] = np.array([np.average(conc_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) #TODO optiize this computation (the iteration) + inflow_mean_c[1:] = np.array([np.average(inflow_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) + Uptake_mean_c[1:] = np.array([np.average(Uptake_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) - """ - return 1 + return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r_prhiz, c_bulk, Vmax, Km, Ds, waterflow, sp, n_approx = 1): @@ -609,8 +592,9 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r n_segments = len(c_bulk) #the radii at which the solute concentration will be tested. They are given as relative values between r_root and r_prhiz. r=1 means r_prhiz. + # approx(int[fun,0,1]) = sum(weights[i] * fun(x(i))) if n_approx ==1: - test_radii = [r_prhiz] + x = [1] weights = [1] else: [x,weights] = ts_roots(n_approx) # returns the nodes and weights for the chebyshev numterical integration @@ -618,18 +602,14 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r rho = [r_prhiz[i]/r_root[i] for i in range(n_segments)] c_sol_mean2root = np.zeros(n_segments) # ratio of the mean solute concentration to solute concentration next to the root - c_sol_prhiz2root = np.zeros(n_segments) mean_watercontent = np.zeros(n_segments) #watercontent next to the root rsc = np.zeros(n_segments) #solute concentration next to the root (output) - F = np.zeros(n_segments) #F is a helper values + F = np.zeros(n_segments) #F is a helper value for computing the ratio between mean solute concentraiton and solute concentration next to the root F0 = np.zeros(n_segments) #helper value for the ratio of solute next to the root #compute a prefactor D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) - #root watercontent - #root_watercontent = [vg.water_content(vg.fast_imfp[self.sp](Phi_root[i]),self.sp) for i in range(n_segments)] - #determine mfp depending on r Phi_A = np.zeros(n_segments) Phi_C = np.zeros(n_segments) @@ -639,171 +619,66 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r #determine F0 - if self.lookup_table_solutes: - F0=[self.lookup_table_solutes((Phi_root[i],0)) for i in range(n_segments)] + if self.lookup_table_sr_solutes_simplified: + F0=[self.lookup_table_sr_solutes_simplified((Phi_root[i],0)) for i in range(n_segments)] else: - F0=[self.integral_overDiffusion_(Phi_root[i],self.sp) for i in range(n_segments)] + F0=[self.integral_AdvectionDiffusion_(Phi_root[i],self.sp) for i in range(n_segments)] + #compute the ratio of (c_mean-C_SW) to (c_root-C_SW) (weighted by water content and cylinder volume) for j in range(n_approx): - test_radii = np.array([math.sqrt(x[j] * (r_prhiz[i]**2-r_root[i]**2)+r_root[i]**2) for i in range(n_segments)]) #scale according to volume + test_radii = np.array([math.sqrt(x[j] * (r_prhiz[i]**2-r_root[i]**2)+r_root[i]**2) for i in range(n_segments)]) #scale the chebyshev nodes according to volume s = [test_radii[i]/r_prhiz[i] for i in range(n_segments)] Phi_current = [Phi_A[i]* (s[i]**2-2*np.log(s[i])) + Phi_C[i] for i in range(n_segments)] current_watercontent = [vg.water_content(vg.fast_imfp[self.sp](Phi_current[i]),self.sp) for i in range(n_segments)] - if self.lookup_table_solutes: - F=[(self.lookup_table_solutes((Phi_current[i],0))-F0[i]) for i in range(n_segments)] + if self.lookup_table_sr_solutes_simplified: + F=[(self.lookup_table_sr_solutes_simplified((Phi_current[i],0))-F0[i]) for i in range(n_segments)] else: - F=[(self.integral_overDiffusion_(Phi_current[i],self.sp)-F0[i]) for i in range(n_segments)] + F=[(self.integral_AdvectionDiffusion_(Phi_current[i],self.sp)-F0[i]) for i in range(n_segments)] for i in range(n_segments): c_sol_mean2root[i] += weights[j] * math.exp(D_tilde*F[i]) * current_watercontent[i] mean_watercontent[i] += weights[j] * current_watercontent[i] for i in range(n_segments): c_sol_mean2root[i] = c_sol_mean2root[i] / mean_watercontent[i] - Phi_outer = [Phi_A[i] + Phi_C[i] for i in range(n_segments)] - if self.lookup_table_solutes: - F=[(self.lookup_table_solutes((Phi_outer[i],0))-F0[i]) for i in range(n_segments)] - else: - F=[(self.integral_overDiffusion_(Phi_outer[i],self.sp)-F0[i]) for i in range(n_segments)] - for i in range(n_segments): - c_sol_prhiz2root[i] = math.exp(D_tilde*F[i]) - - print("c_sol_mean2root", c_sol_mean2root, c_sol_prhiz2root) #solve quadratic eqation # TODO: Link to publication - for i in range(n_segments): - a1=c_bulk[i]/c_sol_mean2root[i]/ + a1=c_bulk[i]/c_sol_mean2root[i] a2=(1-1/c_sol_mean2root[i])/(waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 - rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) - - - return rsc - - - def soil_root_solutes_sr_(self, Phi_root, Phi_soil, rho, c_bulk, Vmax, Km, Ds, waterflow, sp, approximate_rho = False): - """ - finds solute concentration at the soil root interface for all segments assuming steady rate - largely reuses approach of the steady state case, it just needs to compute the factor between mean concentration and the concentration at the outer boundary - uses a look up tables if present (see create_integralconcentration_lookup, create_integralDiffusion_lookup, and open_lookup_ss_solutes, open_lookup_sr_solutes) - - Phi_root matrix flux potential at the root-soil-interface [cm2/d] - Phi_soil matrix flux potential at the bulk soil [cm2/d] - rho geometry factor (outer_radius / inner_radius) [1] - c_bulk solute concentration at the bulk soil [mol/cm3] - Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] - Km half saturation constant Michaelis Menten Kinetics [mol/cm3] - Ds Diffusion constant in water [cm2/d] - waterflow steady state waterflow (entire root circumference) [cm2/d] - sp van Genuchten parameter set - approximate_rho set rho = 0.01 in order to use a small lookup table - """ - assert len(Phi_root) == len(Phi_soil) == len(rho) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" - - n_segments = len(c_bulk) - - rsc = np.zeros(n_segments) - F = np.zeros(n_segments) #F is a helper value - F_tilde_inv = np.zeros(n_segments) - R_sr = np.zeros(n_segments) - watercontent = np.zeros(n_segments) - - #repeat from steady state - if self.lookup_table_solutes: - F=[(self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) for i in range(0, len(c_bulk))] - #F = np.zeros(len(c_bulk)) - #for i in range(0, len(c_bulk)): - # print(Phi_soil[i],Phi_root[i]) - # F[i] = (self.lookup_table_solutes((Phi_soil[i],0))-self.lookup_table_solutes((Phi_root[i],0))) - else: - F=[(self.integral_overDiffusion_(Phi_soil[i],self.sp)-self.integral_overDiffusion_(Phi_root[i],self.sp)) for i in range(0, len(c_bulk))] - - #compute a prefactor - C_d = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) - - if approximate_rho: - if self.lookup_table_sr_solutes: - #print(C_d, Phi_root, Phi_soil) - R_sr=[self.lookup_table_sr_solutes((Ds, max(Phi_root[i],1.0e-5), min(Phi_soil[i],299))) for i in range(0, len(c_bulk))] + if p<0: + rsc[i]=-p/2-math.sqrt(pow(p/2,2)-q) else: - R_sr=[self.integral_overconcentration_(Ds, Phi_root[i], Phi_soil[i], 100.0, self.sp) for i in range(0, len(c_bulk))] - else: - R_sr=[self.integral_overconcentration_(Ds, Phi_root[i], Phi_soil[i], rho[i], self.sp) for i in range(0, len(c_bulk))] - - watercontent = vg.water_content(vg.fast_imfp[sp](Phi_soil), self.sp) #mean watercontent of the perirhizal zone - #reuse steady state case - #solve quadratic eqation # TODO: Link to publication - for i in range(n_segments): - #print("C_d",C_d,"F",F[i]) - F_tilde_inv[i]=math.exp(-C_d*F[i]) - a1=(c_bulk[i]*watercontent[i])/(R_sr[i])*F_tilde_inv[i] - a2=(1-watercontent[i]/R_sr[i]*F_tilde_inv[i])/waterflow[i] - p=Km[i]-a2*Vmax[i]-a1 - q=-Km[i]*a1 - if q>0: - print("q>0",Km[i],a1,F_tilde_inv[i],R_sr[i],c_bulk[i],watercontent[i]) - q=0 - rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) + rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) - return rsc @staticmethod - def integral_overDiffusion_(Phi, sp): + def integral_AdvectionDiffusion_(Phi_input, sp): """ - Integral of (D_s(theta_s-theta_r)^(13/3))/(theta*D(theta)) from Phi = 0.001 to Phi + Integral of (D_s(theta_s-theta_r)^(13/3))/(theta*D(theta)) from Phi = 0.001 to Phi_input Phi matrix flux potential [cm2/d] sp soil parameter: van Genuchten parameter set (type vg.Parameters) """ - if Phi <=0: + if Phi_input <=0: return 0 theta_rel = sp.theta_R/(sp.theta_S-sp.theta_R) - #print("Phi",Phi) integral_fun = lambda Phi: pow(theta_rel+vg.effective_saturation(vg.fast_imfp[sp](Phi),sp),-13/3) - integral_overD, _ = integrate.quad(integral_fun, 1.0e-3, Phi) + integral_AdvDiff, _ = integrate.quad(integral_fun, 1.0e-3, Phi_input) - return integral_overD + return integral_AdvDiff - # deprecate this function. It is way to much work and the steady rate solution isn't even close to accurate - # instead the outer concentration can be used - def integral_overconcentration_(self, Ds, Phi_root, Phi_soil, rho, sp): #TODO: use Phi_A and Phi_C as inputs here so that rho can more easily be varied - """ - (2 pi * 0.9999 rprhiz^2 (c_rs-C_SW))/(Integral from 0.01*rprhiz to rprhiz of 2 pi s (c(s)-C_SW) ds) - - Ds Diffusion coefficient [cm2/d] - Phi_root, Phi_soil matrix flux potential at the root and soil [cm2/d] - rho r_prhiz / r_root - - """ - - Phi_A, Phi_C = self.determine_mfp_function(Phi_root, Phi_soil, rho) - - C_d = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) - - if self.lookup_table_solutes: - #F0=self.lookup_table_solutes(Phi_A*(1/(rho*rho)-np.log(1/(rho*rho)))+Phi_C) - #F0=self.lookup_table_solutes(Phi_A*(0.53**2-2*np.log(0.53))+Phi_C) - #print("Phi",Phi_A*(1.0**2-2*np.log(1.0))+Phi_C) - F0=self.lookup_table_solutes((Phi_A*(1.0**2-2*np.log(1.0))+Phi_C,0.0)) - #print("Phi2",Phi_A*(s2-np.log(s2))+Phi_C) - integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](Phi_A*(s2-np.log(s2))+Phi_C), self.sp)*math.exp(C_d*(self.lookup_table_solutes((Phi_A*(s2-np.log(s2))+Phi_C,0))-F0)) - else: - #F0=self.integral_overDiffusion_(Phi_A*(1/(rho*rho)-np.log(1/(rho*rho)))+Phi_C, self.sp) - #F0=self.integral_overDiffusion_(Phi_A*(0.53**2-2*np.log(0.53))+Phi_C, self.sp) - F0=self.integral_overDiffusion_(Phi_A*(1.0**2-2*np.log(1.0))+Phi_C, self.sp) - integral_fun = lambda s2: vg.water_content(vg.fast_imfp[sp](Phi_A*(s2-np.log(s2))+Phi_C), self.sp)*math.exp(C_d*(self.integral_overDiffusion_(Phi_A*(s2-np.log(s2))+Phi_C, self.sp)-F0)) - integral_R_sr, _ = integrate.quad(integral_fun, 1/(rho*rho), 1.0**2) - R_sr = integral_R_sr / ((1.0**2-1/(rho*rho))) - #print(integral_fun(1/(rho*rho)),integral_fun(0.53**2),integral_fun(1.0**2),integral_R_sr, rho) - print("R_sr",R_sr) - return R_sr def determine_mfp_function(self, Phi_root, Phi_soil, rho): """ determine the spatial function of the mfp + Phi_root matrix flux potential at the root-soil-interface [cm2/d] + Phi_soil matrix flux potential at the bulk soil [cm2/d] + rho geometry factor (outer_radius / inner_radius) [1] Phi(r/r_prhiz)= A(s^2-ln(s^2))+C + (steady rate kinetics with a no flux outer BC at s = 1 A (0.53^2-ln(0.53^2))+C=Phi_soil A (0.01^2-ln(0.01^2))+C=Phi_root @@ -1061,10 +936,10 @@ def create_lookup_global(self, filename, dummy_sp = vg.Parameters([0.078, 0.43, self.global_lookup_table = RegularGridInterpolator((vg_m_, inner_kr_b_, base_mfp_), interface) self.sp = dummy_sp - print("Doen with creating a general lookup table for the matrix flux potential") + print("Done with creating a general lookup table for the matrix flux potential") return 0 - def create_integralDiffusion_lookup(self, filename, sp): + def create_integralAdvectionDiffusion_lookup(self, filename, sp): """ Precomputes all integrals for the steady state solute flow @@ -1081,7 +956,7 @@ def create_integralDiffusion_lookup(self, filename, sp): print(i, " / ", Phin) integral_overD_[i,0] = PerirhizalPython.integral_overDiffusion_(Phi, sp) integral_overD_[i,1] = integral_overD_[i,0] - np.savez(filename, integral_overD_ = integral_overD_, base_mfp_ = base_mfp_, soil = list(sp)) + np.savez(filename, integral_AdvDiff_ = integral_AdvDiff_, base_mfp_ = base_mfp_, soil = list(sp)) self.lookup_table_solutes = RegularGridInterpolator((base_mfp_,[0,1]) , integral_overD_) self.sp = sp return integral_overD_, base_mfp_ From f994aa83b223ee5f431024cb0da6b77e8ebb807d Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Wed, 8 Jul 2026 10:13:07 +0200 Subject: [PATCH 33/41] steady rate water with inflow --- .../test_perirhizal.py | 42 ++++---- src/functional/Perirhizal.py | 99 ++++++++++++------- 2 files changed, 84 insertions(+), 57 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index f42d9134d..5bbf005a5 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -38,8 +38,8 @@ do_computation = True #should the computation be run or take the data from a saved file # general parameters -max_time = 0.1 # d -n_times = 10 # number of time intervals +max_time = 0.2 # d +n_times = 20 # number of time intervals r_prhiz = 0.6 # perirhizal radius[cm] r_root = 0.02 # root radius [cm] NC = 40 # number of spatial discretisations @@ -244,8 +244,8 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu inflow_s_nf = inflow_s_nf[0] rootuptake_w_g = rootuptake_w_g[0] rootuptake_s_g = rootuptake_s_g[0] - inflow_w_g = inflow_w_g[0] - inflow_s_g = inflow_s_g[0] + inflow_w_g = abs(inflow_w_g[0]) + inflow_s_g = abs(inflow_s_g[0]) #TODO take all the flows to the root as negative? #store the dumux outputs #scenario no flux outer BC @@ -291,9 +291,10 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu Phi_root_nf = Phi_nf(r_root)#mfp next to the root #general outer BC: Dirichlet BC to a fixed potential Phi_soil_g = vg.fast_mfp[peri.sp](mean_waterpotential_g) #mfp of the mean soil - Phi_outer_g = Phi_soil_g - (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_g*r_prhiz)*r_prhiz*np.log(0.53) #mfp at the outer perirhizal radius - Phi_g = lambda r: Phi_outer_g + (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_g*r_prhiz)*r_prhiz*np.log(r/r_prhiz)#mfp function depending on radius + Phi_outer_g = Phi_soil_g - (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_g*r_prhiz)*np.log(0.53) #mfp at the outer perirhizal radius + Phi_g = lambda r: Phi_outer_g + (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_g*r_prhiz)*np.log(r/r_prhiz)#mfp function depending on radius Phi_root_g = Phi_nf(r_root)#mfp next to the root + print("inflow", inflow_w_g) #write the steady rate approximations for the water as outputs waterpotential_sr[0,r,0] = rootuptake_w_nf @@ -345,26 +346,27 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #case of general steady rate water uptake #for the steady state take again the outer concentration #TODO: check weather Vmax or Vmax per area - rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_soil_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ss") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ss") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) solutes_dumux_ss[1,r,0] = -Uptake[0] solutes_dumux_ss[1,r,1] = -ss_uptake[0] solutes_dumux_ss[1,r,2:] = soluteconcentration[:] solutes_dumux_ss[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate no flux outer BC - #rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_soil_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr") - #_, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) - #solutes_dumux_sr[1,r,0] = -Uptake[0] - #solutes_dumux_sr[1,r,1] = -ss_uptake[0] - #solutes_dumux_sr[1,r,2:] = soluteconcentration[:] - #solutes_dumux_sr[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area + rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + solutes_dumux_sr[1,r,0] = -Uptake[0] + solutes_dumux_sr[1,r,1] = -ss_uptake[0] + solutes_dumux_sr[1,r,2:] = soluteconcentration[:] + solutes_dumux_sr[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate solute uptake with the farfield approximation - #rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_soil_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ff") - #_, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_nf, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) - #solutes_dumux_ff[1,r,0] = -Uptake[0] - #solutes_dumux_ff[1,r,1] = -ss_uptake[0] - #solutes_dumux_ff[1,r,2:] = soluteconcentration[:] - #solutes_dumux_ff[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area + rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ff") + _, waterpotential, soluteconcentration = peri.watersolutes_disc(vg.fast_mfp[peri.sp](waterpotential_dumux[1,r,-1]), 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + waterpotential_sr[1,r,2:] = waterpotential #TODO: this is a test + solutes_dumux_ff[1,r,0] = -Uptake[0] + solutes_dumux_ff[1,r,1] = -ss_uptake[0] + solutes_dumux_ff[1,r,2:] = soluteconcentration[:] + solutes_dumux_ff[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area return watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_dumux_sr, solutes_dumux_ss, solutes_dumux_ff diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index e7e5dd4db..aee0485fe 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -282,11 +282,11 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, return rsc - def soil_root_solutes_sr(self, Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km, Ds, sp, mode = "ff"): + def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km, Ds, sp, mode = "ff"): """ steady rate assumption of solute uptake by roots TODO: insert citaiton - Phi_soil outer matrix flux potential [cm2/d] + Phi_out outer matrix flux potential [cm2/d] rootwateruptake radial root water uptake [cm2/d] waterinflow radial water inflow at r_prhiz [cm2/d] r_root root radius [cm] @@ -307,7 +307,7 @@ def soil_root_solutes_sr(self, Phi_soil, rootwateruptake, waterinflow, r_root, r Uptake solute uptake of the root [mol/(cm d)] """ - assert len(Phi_soil) == len(rootwateruptake) == len(waterinflow) == len(r_root) == len(r_prhiz) == len(c_soil) == len(c_outer) == len(Vmax) == len(Km), "Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax and Km must have the same length" + assert len(Phi_out) == len(rootwateruptake) == len(waterinflow) == len(r_root) == len(r_prhiz) == len(c_soil) == len(c_outer) == len(Vmax) == len(Km) == len(Ds), "Phi_soil, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km and Ds must have the same length" n_segments = len(c_soil) @@ -319,28 +319,36 @@ def soil_root_solutes_sr(self, Phi_soil, rootwateruptake, waterinflow, r_root, r #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): rho = r_prhiz[i] / r_root[i] - Phi_out = Phi_soil[i] -( (rootwateruptake[i]-waterinflow[i])*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow[i]*np.log(0.53) ) + Phi_0 = Phi_out[i] - 1/2 *(rootwateruptake[i] - waterinflow[i]) * rho**2 / (1 - rho**2) + Phi_1 = waterinflow[i] - 1/2 * (rootwateruptake[i] - waterinflow[i]) * rho**2 / (1 - rho**2) + Phi_2 = (rootwateruptake[i] - waterinflow[i]) * rho**2 / (1 - rho**2) + Phi = lambda r : Phi_2 * (r/r_prhiz[i])**2 + Phi_1 * np.log(r/r_prhiz[i]) + Phi_0 + #Phi_out = Phi_soil[i] -( (rootwateruptake[i]-waterinflow[i])*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow[i]*np.log(0.53) ) #Phi = lambda r_rel : Phi_out + (rootwateruptake[i]-waterinflow[i])*((r_rel**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) #TODO: test using Phi_soil instead of Phi_outer - Phi = lambda r : Phi_out + (rootwateruptake[i] - waterinflow[i]) * ((r/r_prhiz[i])**2/2-1/2+np.log(r_prhiz[i]/r)) * rho**2 / (1 - rho**2) + waterinflow[i] * np.log(r / r_prhiz[i]) - Phi = lambda r : 1.0 #TODO: remove times 3 + #Phi = lambda r : Phi_out[i] + (rootwateruptake[i] - waterinflow[i]) * ((r/r_prhiz[i])**2/2-1/2+np.log(r_prhiz[i]/r)) * rho**2 / (1 - rho**2) + waterinflow[i] * np.log(r / r_prhiz[i]) + #Phi = lambda r : 1.0 #TODO: remove times 3 radial_waterflow = lambda r : (waterinflow[i] + (rootwateruptake[i]-waterinflow[i]) * (r_prhiz[i]**2 - r**2) / (r_prhiz[i]**2 - r_root[i]**2)) / (2 * np.pi * r) Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel)),sp),10/3) / (sp.theta_S**2) # Millington and Quirk Ds0 = self.Ds0_ref - scaling = math.sqrt(Ds[i] / Ds0) #I am an idiot I am an idiot. I messed up the ordering - Phi1 = Phi(1/scaling) - waterinflow[i] * np.log(scaling) - (rootwateruptake[i] - waterinflow[i]) * r_prhiz[i]**2 / (r_prhiz[i]**2 - r_root[i]**2) * (np.log(scaling)-1/2 * (1-1/scaling**2)) - waterflow = radial_waterflow(scaling) / scaling - wateruptake = (rootwateruptake[i] - waterinflow[i]) * r_prhiz[i]**2 / (r_prhiz[i]**2 - r_root[i]**2) / scaling**2 + scaling = math.sqrt(Ds[i] / Ds0) # Ds_i > Ds0 means scaling >1, the region is modeled to be smaller than it actually is + Phi_2 = Phi_2 * (scaling**2) + Phi_1 = Phi_1 + Phi_0 = Phi_0 + Phi_1 * np.log(scaling) + radial_waterflow = lambda r : 4 * np.pi * (r**2) * Phi_2 + 2 * np.pi * Phi_1 #for the new scaling + #Phi = lambda r : Phi_2 * (r/r_prhiz[i])**2 + Phi_1 * np.log(r/r_prhiz[i]) + Phi_0 + #Phi1 = Phi(scaling*) - waterinflow[i] * np.log(scaling) - (rootwateruptake[i] - waterinflow[i]) / (r_prhiz[i]**2 - r_root[i]**2) * (np.log(scaling) * r_prhiz[i]**2 - 1/2 * (r_prhiz[i]**2-1/scaling**2)) + #wateruptake = (rootwateruptake[i] - waterinflow[i]) / (r_prhiz[i]**2 - r_root[i]**2) * scaling**2 #* r_prhiz[i]**2 + #waterflow = radial_waterflow(scaling*r_prhiz[i]) - (rootwateruptake[i]-waterinflow[i]) / (r_prhiz[i]**2 - r_root[i]**2) * r_prhiz[i]**2 * (1 - scaling**2) - r_eval = np.logspace(np.log10(r_root[i]), np.log10(r_prhiz[i]), num = 40) - r_eval = [r * scaling for r in r_eval] - Phi1 = 1 - waterflow = 0 - wateruptake = 0 - print("r_eval equation before",r_root[i], r_prhiz[i],r_eval) - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi1, waterflow, wateruptake, r_eval, sp) + #TODO: implement using the lookup table here - print("r_eval equation",r_root[i], r_prhiz[i],r_eval) + r_eval = np.logspace(np.log10(r_root[i]), np.log10(r_prhiz[i]), num = 40) + r_eval = [r * scaling / r_prhiz[i] for r in r_eval] + #Phi1 = 1 + #waterflow = 0 + #wateruptake = 0 + conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) #default prefactors for the steady state case pre_c = 0 @@ -457,37 +465,48 @@ def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prh #water #Phi_out = Phi_soil -( (rootwateruptake-waterinflow)*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow*np.log(0.53) ) - Phi = lambda r : Phi_out - (rootwateruptake - waterinflow) * ((r/r_prhiz)**2/2-1/2+np.log(r_prhiz/r)) * r_prhiz**2 / (r_prhiz**2 - r_root**2) + waterinflow * np.log(r / r_prhiz) - Phi = lambda r : 1.0 #TODO: remove times 3 + Phi = lambda r : Phi_out + (rootwateruptake - waterinflow) / (2*np.pi) * ((r/r_prhiz)**2/2-1/2+np.log(r_prhiz/r)) *(rho**2)/(1-rho**2) + waterinflow / (2*np.pi) * np.log(r / r_prhiz) + #Phi = lambda r : 1.0 #TODO: remove times 3 wateruptake = (rootwateruptake - waterinflow) / (np.pi * (r_prhiz**2 - r_root**2))#TODO doublecheck radial_waterflow = lambda r : (waterinflow + wateruptake * np.pi * (r_prhiz**2 - r**2)) / (2 * np.pi * r) #TODO: rename + #waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) + #watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) + + rho = r_prhiz / r_root + Phi_0 = Phi_out + 1/2 *(rootwateruptake - waterinflow) * rho**2 / (1 - rho**2) + Phi_1 = waterinflow + (rootwateruptake - waterinflow) * rho**2 / (1 - rho**2) + Phi_2 = -(rootwateruptake - waterinflow) * rho**2 / (1 - rho**2) + #Phi = lambda r : Phi_2 * (r/r_prhiz)**2/2 + Phi_1 * np.log(r/r_prhiz) + Phi_0 waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) - #solutes #use the subfunction #TODO: alternative lookup table - scaling = math.sqrt(Ds0 / self.Ds0_ref) + scaling = math.sqrt(Ds0 / self.Ds0_ref) # Ds0 > Ds0_ref means scaling >1, the region is modeled to be smaller than it actually is Phi1 = Phi(1/scaling) - waterinflow * np.log(scaling) - (rootwateruptake - waterinflow) * r_prhiz**2 / (r_prhiz**2 - r_root**2) * (np.log(scaling)-1/2 * (1-1/scaling**2)) waterflow = radial_waterflow(scaling) / scaling - r_eval = [r * scaling for r in r_eval] - wateruptake = (rootwateruptake - waterinflow) * r_prhiz**2 / (r_prhiz**2 - r_root**2) / scaling**2 + #r_eval = [r * scaling / r_prhiz for r in r_eval] + wateruptake = (rootwateruptake - waterinflow) / (r_prhiz**2 - r_root**2) / scaling**2 #* r_prhiz**2 + + Phi_2 = Phi_2 * (scaling**2) + Phi_1 = Phi_1 + Phi_0 = Phi_0 + Phi_1 * np.log(scaling) + radial_waterflow = lambda r : 2 * np.pi * (r**2) * Phi_2 + 2 * np.pi * Phi_1 #for the new scaling - Phi1 = 1 - waterflow = 0 - wateruptake = 0 + #Phi1 = 1 + #waterflow = 0 + #wateruptake = 0 Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) Km = 1.5e-7 #mol/cm3 #rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([Phi_out], [rootwateruptake], [waterinflow], [r_root], [r_prhiz], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") - rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([1], [0], [0], [r_root], [r_prhiz], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") - #conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi1, waterflow, wateruptake, r_eval, sp) - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(1, 0, 0, r_eval, sp) - c_root = rsc - print("compareinflow", inflow_mean_c_0, inflow_mean_c) - print("r_eval disc", r_eval) + #rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([1], [0], [0], [r_root], [r_prhiz], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") + conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) + #conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(1, 0, 0, r_eval, sp) + #c_root = rsc + #print("compareinflow", inflow_mean_c_0, inflow_mean_c) soluteconcentration = c_root * conc_rel_c + ss_uptake * inflow_rel_c + sr_uptake * Uptake_rel_c soluteconcentration_mean = c_root * conc_mean_c + ss_uptake * inflow_mean_c + sr_uptake * Uptake_mean_c @@ -502,7 +521,7 @@ def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prh return watercontent, waterpotential, soluteconcentration - def solute_linearequation_sr_(self, Phi1, waterflow, wateruptake, r_eval, sp): + def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): """ computes a linear equation from the diffusion advection ODE on the perirhizal solute flow #TODO: insert equation number the radius (and steady rate wateruptake) are scaled by the diffusion coefficient of the solute @@ -538,12 +557,18 @@ def solute_linearequation_sr_(self, Phi1, waterflow, wateruptake, r_eval, sp): watercontent_times_r = np.ones(n) #waterflow - Phi = lambda r : Phi1 - wateruptake * (r**2/2 - 1/2 - np.log(r)) + waterflow * 2 * np.pi * 1 * np.log(r) # TODO add citation - Phi = lambda r : 1.0 #TODO: remove times 3 - radial_waterflow = lambda r : (2 * np.pi * 1 * waterflow + wateruptake * np.pi * (1 - r**2)) #TODO: distinct waterflow [cm/d] and radial waterflow [cm2/d] + #Phi = lambda r : Phi1 - wateruptake * (r**2/2 - 1/2 - np.log(r)) + waterflow * 2 * np.pi * 1 * np.log(r) # TODO add citation + #Phi = lambda r : 1.0 #TODO: remove times 3 + #radial_waterflow = lambda r : (2 * np.pi * 1 * waterflow + wateruptake * np.pi * (1 - r**2)) #TODO: distinct waterflow [cm/d] and radial waterflow [cm2/d] + #watercontent = lambda r : vg.water_content(vg.fast_imfp[sp](Phi(r)), sp) + #print("what is python doing?", watercontent(0.1), Ds0, Phi(0.1)) + Phi = lambda r : Phi_2 * r**2 + Phi_1 * np.log(r) + Phi_0 + radial_waterflow = lambda r : 4 * np.pi * (r**2) * Phi_2 + 2 * np.pi * Phi_1 #for the new scaling watercontent = lambda r : vg.water_content(vg.fast_imfp[sp](Phi(r)), sp) Ds = lambda r : Ds0 * math.pow(watercontent(r),10/3) / (sp.theta_S**2) + + f_homogen = lambda c, r : ( radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference f_steadystate = lambda c, r : (1 + radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference f_quadratic = lambda c, r : (r**2 + radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference From dc54f59ba88e3696cc11c37bc4c9b2249ac0fd52 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Wed, 8 Jul 2026 13:00:56 +0200 Subject: [PATCH 34/41] steady state solute flow --- .../test_perirhizal.py | 8 +- src/functional/Perirhizal.py | 108 ++++++++---------- 2 files changed, 53 insertions(+), 63 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index 5bbf005a5..53328d7b5 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -38,8 +38,8 @@ do_computation = True #should the computation be run or take the data from a saved file # general parameters -max_time = 0.2 # d -n_times = 20 # number of time intervals +max_time = 0.1 # d +n_times = 10 # number of time intervals r_prhiz = 0.6 # perirhizal radius[cm] r_root = 0.02 # root radius [cm] NC = 40 # number of spatial discretisations @@ -361,8 +361,8 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_dumux_sr[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate solute uptake with the farfield approximation rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ff") - _, waterpotential, soluteconcentration = peri.watersolutes_disc(vg.fast_mfp[peri.sp](waterpotential_dumux[1,r,-1]), 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) - waterpotential_sr[1,r,2:] = waterpotential #TODO: this is a test + _, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + waterpotential_sr[1,r,2:] = waterpotential #TODO: this is a test #vg.fast_mfp[peri.sp](waterpotential_dumux[1,r,-1]) solutes_dumux_ff[1,r,0] = -Uptake[0] solutes_dumux_ff[1,r,1] = -ss_uptake[0] solutes_dumux_ff[1,r,2:] = soluteconcentration[:] diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index aee0485fe..eb547666d 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -319,32 +319,32 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): rho = r_prhiz[i] / r_root[i] - Phi_0 = Phi_out[i] - 1/2 *(rootwateruptake[i] - waterinflow[i]) * rho**2 / (1 - rho**2) - Phi_1 = waterinflow[i] - 1/2 * (rootwateruptake[i] - waterinflow[i]) * rho**2 / (1 - rho**2) - Phi_2 = (rootwateruptake[i] - waterinflow[i]) * rho**2 / (1 - rho**2) - Phi = lambda r : Phi_2 * (r/r_prhiz[i])**2 + Phi_1 * np.log(r/r_prhiz[i]) + Phi_0 - #Phi_out = Phi_soil[i] -( (rootwateruptake[i]-waterinflow[i])*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow[i]*np.log(0.53) ) - #Phi = lambda r_rel : Phi_out + (rootwateruptake[i]-waterinflow[i])*((r_rel**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/r_rel)-0.5)) + waterinflow[i]*np.log(r_rel) #TODO: test using Phi_soil instead of Phi_outer - #Phi = lambda r : Phi_out[i] + (rootwateruptake[i] - waterinflow[i]) * ((r/r_prhiz[i])**2/2-1/2+np.log(r_prhiz[i]/r)) * rho**2 / (1 - rho**2) + waterinflow[i] * np.log(r / r_prhiz[i]) - #Phi = lambda r : 1.0 #TODO: remove times 3 - radial_waterflow = lambda r : (waterinflow[i] + (rootwateruptake[i]-waterinflow[i]) * (r_prhiz[i]**2 - r**2) / (r_prhiz[i]**2 - r_root[i]**2)) / (2 * np.pi * r) - Ds_func = lambda r_rel : Ds[i] * math.pow(vg.water_content(vg.fast_imfp[sp](Phi(r_rel)),sp),10/3) / (sp.theta_S**2) # Millington and Quirk + Phi_0 = Phi_out[i] - 1/2 *(rootwateruptake[i] - waterinflow[i]) / (2*np.pi) * (rho**2) / (1 - rho**2) + Phi_1 = waterinflow[i] / (2*np.pi) - (rootwateruptake[i] - waterinflow[i]) / (2*np.pi) * rho**2 / (1 - rho**2) + Phi_2 = (rootwateruptake[i] - waterinflow[i]) / (2*np.pi) * (rho**2) / (1 - rho**2) + #are these next few lines even necessary? + Phi = lambda r : Phi_2 * (r/r_prhiz)**2/2 + Phi_1 * np.log(r/r_prhiz) + Phi_0 #warning: changing Phi_0 will also change this lambda function + waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) + watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) + radial_waterflow = lambda r : 2 * np.pi * (Phi_2 * (r/r_prhiz)**2 + Phi_1) + Ds_func = lambda r : Ds[i] * math.pow(watercontent_func(r),10/3) / (sp.theta_S**2) # Millington and Quirk Ds0 = self.Ds0_ref scaling = math.sqrt(Ds[i] / Ds0) # Ds_i > Ds0 means scaling >1, the region is modeled to be smaller than it actually is - Phi_2 = Phi_2 * (scaling**2) + scaling = Ds[i] / Ds0 # Ds_i > Ds0 means scaling >1, the region is modeled to be smaller than it actually is + Phi_2 = Phi_2 * ((scaling*r_prhiz[i])**2) Phi_1 = Phi_1 - Phi_0 = Phi_0 + Phi_1 * np.log(scaling) - radial_waterflow = lambda r : 4 * np.pi * (r**2) * Phi_2 + 2 * np.pi * Phi_1 #for the new scaling - #Phi = lambda r : Phi_2 * (r/r_prhiz[i])**2 + Phi_1 * np.log(r/r_prhiz[i]) + Phi_0 - #Phi1 = Phi(scaling*) - waterinflow[i] * np.log(scaling) - (rootwateruptake[i] - waterinflow[i]) / (r_prhiz[i]**2 - r_root[i]**2) * (np.log(scaling) * r_prhiz[i]**2 - 1/2 * (r_prhiz[i]**2-1/scaling**2)) - #wateruptake = (rootwateruptake[i] - waterinflow[i]) / (r_prhiz[i]**2 - r_root[i]**2) * scaling**2 #* r_prhiz[i]**2 - #waterflow = radial_waterflow(scaling*r_prhiz[i]) - (rootwateruptake[i]-waterinflow[i]) / (r_prhiz[i]**2 - r_root[i]**2) * r_prhiz[i]**2 * (1 - scaling**2) + Phi_0 = Phi_0 + Phi_1 * np.log(scaling*r_prhiz[i]) + Phi = lambda r : Phi_2 * r**2/2 + Phi_1 * np.log(r) + Phi_0 #warning: changing Phi_0 will also change this lambda function + waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) #is the watercontent really necessary here? + watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) + radial_waterflow = lambda r : 2 * np.pi * (Phi_2 * r**2 + Phi_1) + Ds_func = lambda r : Ds[i] * math.pow(watercontent_func(r),10/3) / (sp.theta_S**2) # Millington and Quirk #TODO: implement using the lookup table here r_eval = np.logspace(np.log10(r_root[i]), np.log10(r_prhiz[i]), num = 40) - r_eval = [r * scaling / r_prhiz[i] for r in r_eval] + r_eval = [r / scaling / r_prhiz[i] for r in r_eval] #Phi1 = 1 #waterflow = 0 #wateruptake = 0 @@ -399,7 +399,8 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ #ss flow + sr uptake = Uptake A[2,0] = 1 - A[2,1] = 1 + #A[2,1] = 1 + A[2,1] = 0 A[2,3] = -1 #pre_c * c(r_prhiz) + pre_srUptake * srUptake + pre_inflow * inflow (= ssflow) = absolute @@ -437,7 +438,7 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ print("output", conc_mean_c[-1], inflow_mean_c[-1], Uptake_mean_c[-1], c_soil) print("linear", rsc*conc_mean_c[-1]+ss_uptake* inflow_mean_c[-1]+sr_uptake* Uptake_mean_c[-1], c_soil) - + #print("r_eval", r_eval, Phi_0, Phi_1, Phi_2) return rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prhiz, r_eval, c_root, Ds0, ss_uptake, sr_uptake, sp): @@ -462,47 +463,42 @@ def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prh #simple computations rho = r_prhiz / r_root - - #water - #Phi_out = Phi_soil -( (rootwateruptake-waterinflow)*((0.53**2)*(rho**2)/(2*(1-rho**2))+rho**2/(1-rho**2)*(np.log(1/0.53)-0.5)) + waterinflow*np.log(0.53) ) - Phi = lambda r : Phi_out + (rootwateruptake - waterinflow) / (2*np.pi) * ((r/r_prhiz)**2/2-1/2+np.log(r_prhiz/r)) *(rho**2)/(1-rho**2) + waterinflow / (2*np.pi) * np.log(r / r_prhiz) - #Phi = lambda r : 1.0 #TODO: remove times 3 - wateruptake = (rootwateruptake - waterinflow) / (np.pi * (r_prhiz**2 - r_root**2))#TODO doublecheck - radial_waterflow = lambda r : (waterinflow + wateruptake * np.pi * (r_prhiz**2 - r**2)) / (2 * np.pi * r) #TODO: rename - #waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) - #watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) - - rho = r_prhiz / r_root - Phi_0 = Phi_out + 1/2 *(rootwateruptake - waterinflow) * rho**2 / (1 - rho**2) - Phi_1 = waterinflow + (rootwateruptake - waterinflow) * rho**2 / (1 - rho**2) - Phi_2 = -(rootwateruptake - waterinflow) * rho**2 / (1 - rho**2) - #Phi = lambda r : Phi_2 * (r/r_prhiz)**2/2 + Phi_1 * np.log(r/r_prhiz) + Phi_0 + Phi_0 = Phi_out - 0.5 * (rootwateruptake - waterinflow) / (2*np.pi) * (rho**2) / (1 - rho**2) + Phi_1 = waterinflow / (2*np.pi) - (rootwateruptake - waterinflow) / (2*np.pi) * rho**2 / (1 - rho**2) + Phi_2 = (rootwateruptake - waterinflow) / (2*np.pi) * (rho**2) / (1 - rho**2) + Phi = lambda r : Phi_2 * (r/r_prhiz)**2/2 + Phi_1 * np.log(r/r_prhiz) + Phi_0 #warning: changing Phi_0 will also change this lambda function waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) + radial_waterflow = lambda r : 2 * np.pi * (Phi_2 * (r/r_prhiz)**2 + Phi_1) + watercontent = np.array([watercontent_func(r) for r in r_eval]) + waterpotential = np.array([waterpotential_func(r) for r in r_eval]) + + + + + #solutes #use the subfunction #TODO: alternative lookup table scaling = math.sqrt(Ds0 / self.Ds0_ref) # Ds0 > Ds0_ref means scaling >1, the region is modeled to be smaller than it actually is - Phi1 = Phi(1/scaling) - waterinflow * np.log(scaling) - (rootwateruptake - waterinflow) * r_prhiz**2 / (r_prhiz**2 - r_root**2) * (np.log(scaling)-1/2 * (1-1/scaling**2)) - waterflow = radial_waterflow(scaling) / scaling - #r_eval = [r * scaling / r_prhiz for r in r_eval] - wateruptake = (rootwateruptake - waterinflow) / (r_prhiz**2 - r_root**2) / scaling**2 #* r_prhiz**2 + scaling = Ds0 / self.Ds0_ref # Ds0 > Ds0_ref means scaling >1, the region is modeled to be smaller than it actually is + #Phi1 = Phi(1/scaling) - waterinflow * np.log(scaling) - (rootwateruptake - waterinflow) * r_prhiz**2 / (r_prhiz**2 - r_root**2) * (np.log(scaling)-1/2 * (1-1/scaling**2)) + #waterflow = radial_waterflow(scaling) / scaling + r_eval = [r / scaling / r_prhiz for r in r_eval] + #wateruptake = (rootwateruptake - waterinflow) / (r_prhiz**2 - r_root**2) / scaling**2 #* r_prhiz**2 - Phi_2 = Phi_2 * (scaling**2) + Phi_2 = Phi_2 * ((scaling*r_prhiz)**2) Phi_1 = Phi_1 - Phi_0 = Phi_0 + Phi_1 * np.log(scaling) - radial_waterflow = lambda r : 2 * np.pi * (r**2) * Phi_2 + 2 * np.pi * Phi_1 #for the new scaling + Phi_0 = Phi_0 + Phi_1 * np.log(scaling*r_prhiz) + #radial_waterflow = lambda r : 2 * np.pi * (r**2) * Phi_2 + 2 * np.pi * Phi_1 #for the new scaling - #Phi1 = 1 - #waterflow = 0 - #wateruptake = 0 Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) Km = 1.5e-7 #mol/cm3 #rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([Phi_out], [rootwateruptake], [waterinflow], [r_root], [r_prhiz], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") - #rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([1], [0], [0], [r_root], [r_prhiz], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") + rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([Phi_out], [rootwateruptake], [waterinflow], [r_eval[0]], [r_eval[-1]], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) #conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(1, 0, 0, r_eval, sp) #c_root = rsc @@ -516,15 +512,14 @@ def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prh print("output disc3", conc_rel_c[-1], inflow_rel_c[-1], Uptake_rel_c[-1], soluteconcentration[-1]) print("output disc4", conc_mean_c[-1], inflow_mean_c[-1], Uptake_mean_c[-1], soluteconcentration_mean[-1]) - watercontent = np.array([watercontent_func(r) for r in r_eval]) - waterpotential = np.array([waterpotential_func(r) for r in r_eval]) + return watercontent, waterpotential, soluteconcentration def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): """ computes a linear equation from the diffusion advection ODE on the perirhizal solute flow #TODO: insert equation number - the radius (and steady rate wateruptake) are scaled by the diffusion coefficient of the solute + the radius (and steady rate wateruptake) are scaled by the diffusion coefficient of the solute already included in the inputs Phi1 matrix flux potential at \tilde{r} = 1 [cm2/d] waterflow waterflow at r = 1 [cm/d] @@ -557,18 +552,11 @@ def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): watercontent_times_r = np.ones(n) #waterflow - #Phi = lambda r : Phi1 - wateruptake * (r**2/2 - 1/2 - np.log(r)) + waterflow * 2 * np.pi * 1 * np.log(r) # TODO add citation - #Phi = lambda r : 1.0 #TODO: remove times 3 - #radial_waterflow = lambda r : (2 * np.pi * 1 * waterflow + wateruptake * np.pi * (1 - r**2)) #TODO: distinct waterflow [cm/d] and radial waterflow [cm2/d] - #watercontent = lambda r : vg.water_content(vg.fast_imfp[sp](Phi(r)), sp) - #print("what is python doing?", watercontent(0.1), Ds0, Phi(0.1)) - Phi = lambda r : Phi_2 * r**2 + Phi_1 * np.log(r) + Phi_0 - radial_waterflow = lambda r : 4 * np.pi * (r**2) * Phi_2 + 2 * np.pi * Phi_1 #for the new scaling + Phi = lambda r : Phi_2 * r**2/2 + Phi_1 * np.log(r) + Phi_0 + radial_waterflow = lambda r : - 2 * np.pi * (Phi_2 * r**2 + Phi_1) watercontent = lambda r : vg.water_content(vg.fast_imfp[sp](Phi(r)), sp) Ds = lambda r : Ds0 * math.pow(watercontent(r),10/3) / (sp.theta_S**2) - - f_homogen = lambda c, r : ( radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference f_steadystate = lambda c, r : (1 + radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference f_quadratic = lambda c, r : (r**2 + radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference @@ -586,10 +574,12 @@ def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): inflow_mean_c[0] = inflow_rel_c[0] Uptake_mean_c[0] = Uptake_rel_c[0] watercontent_times_r = np.array([watercontent(r)*r for r in r_eval]) - conc_mean_c[1:] = np.array([np.average(conc_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) #TODO optiize this computation (the iteration) + conc_mean_c[1:] = np.array([np.average(conc_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) inflow_mean_c[1:] = np.array([np.average(inflow_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) Uptake_mean_c[1:] = np.array([np.average(Uptake_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) + print("Phis", Phi_0, Phi_1, Phi_2) + return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c From d68c5e318d7618008d2e1d44f2820fcb4159c5c2 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Wed, 8 Jul 2026 17:32:12 +0200 Subject: [PATCH 35/41] multiple outer BC steady rate solute --- .../test_perirhizal.py | 47 ++--- src/functional/Perirhizal.py | 193 ++++++++---------- 2 files changed, 113 insertions(+), 127 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index 53328d7b5..687c49db3 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -38,11 +38,11 @@ do_computation = True #should the computation be run or take the data from a saved file # general parameters -max_time = 0.1 # d -n_times = 10 # number of time intervals +max_time = 1.0 # d +n_times = 100 # number of time intervals r_prhiz = 0.6 # perirhizal radius[cm] r_root = 0.02 # root radius [cm] -NC = 40 # number of spatial discretisations +NC = 30 # number of spatial discretisations length = 1 #default length of the segment, will not change the outcpme as all variables are constant in this direction [cm] #two scenarios will be computed: one without inflow, another with a Dirichlet BC @@ -50,7 +50,7 @@ #initial conditions initial_waterpotential = -200 -initial_soluteconcentration = 2e-5#mol/cm3 +initial_soluteconcentration = 2e-6#mol/cm3 #space for the outputs # number of tests, number of scenarios, number of timesteps, (rootuptake, inflow, discretisation) @@ -320,8 +320,8 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #result_solutes_ss_nf = peri.soil_root_solutes_ss_([Phi_root_nf], [Phi_outer_nf], [solutes_nf[-1]], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) #result_solutes_sr_nf = peri.soil_root_solutes_sr_([Phi_root_nf], [Phi_outer_nf], [rho], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) - result_solutes_ss_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [abs(waterdemand)], peri.sp, n_approx = 5) - result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [abs(radial_waterdemand)], peri.sp, n_approx = 1) + result_solutes_ss_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [-abs(waterdemand)], peri.sp, n_approx = 5) + result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [-abs(radial_waterdemand)], peri.sp, n_approx = 1) #safe the results #(here there is no computed inflow, so index 1 doesn't do anything) @@ -329,6 +329,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_dumux_ss[0,r,0]=-Vmax_per_area * result_solutes_ss_nf / (Km + result_solutes_ss_nf) result_solutes_sr_nf = result_solutes_sr_nf[0] solutes_dumux_sr[0,r,0]=-Vmax_per_area * result_solutes_sr_nf / (Km + result_solutes_sr_nf) + print("concentration sr nf ", result_solutes_ss_nf, Km, solutes_dumux_ss[0,r,0]) F0_nf = peri.integral_AdvectionDiffusion_(Phi_root_nf,peri.sp) F0_g = peri.integral_AdvectionDiffusion_(Phi_root_g,peri.sp) @@ -340,31 +341,31 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu F_g = peri.integral_AdvectionDiffusion_(Phi_g(r_current),peri.sp)-F0_g F_tilde_nf=math.exp(D_tilde*F_nf) F_tilde_g=math.exp(D_tilde*F_g) - solutes_dumux_ss[0,r,2+j] = result_solutes_ss_nf * F_tilde_nf + (1-F_tilde_nf) * solutes_dumux_ss[0,r,0] / waterdemand#waterdemand is assumed to be negative #TODO: look at the signs - solutes_dumux_sr[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf + (1-F_tilde_nf) * solutes_dumux_sr[0,r,0] / radial_waterdemand #an uptake of both water and solute is assumed + solutes_dumux_ss[0,r,2+j] = result_solutes_ss_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_ss[0,r,0] / waterdemand#waterdemand is assumed to be negative #TODO: look at the signs + solutes_dumux_sr[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_sr[0,r,0] / radial_waterdemand #an uptake of both water and solute is assumed #case of general steady rate water uptake #for the steady state take again the outer concentration #TODO: check weather Vmax or Vmax per area - rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ss") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + rsc, Uptake, quadratic_flow = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, Uptake, quadratic_flow, peri.sp) solutes_dumux_ss[1,r,0] = -Uptake[0] - solutes_dumux_ss[1,r,1] = -ss_uptake[0] + solutes_dumux_ss[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_dumux_ss[1,r,2:] = soluteconcentration[:] solutes_dumux_ss[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate no flux outer BC - rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "sr") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + rsc, Uptake, quadratic_flow = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") + _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, Uptake, quadratic_flow, peri.sp) solutes_dumux_sr[1,r,0] = -Uptake[0] - solutes_dumux_sr[1,r,1] = -ss_uptake[0] + solutes_dumux_sr[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_dumux_sr[1,r,2:] = soluteconcentration[:] solutes_dumux_sr[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area #steady rate solute uptake with the farfield approximation - rsc, Uptake, ss_uptake, sr_uptake, _ = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax_per_area], [Km], [Ds], peri.sp, mode = "ff") - _, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, ss_uptake, sr_uptake, peri.sp) + rsc, Uptake, quadratic_flow = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "dirichlet") #doublecheck Vmax and Vmax per area + _, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, Uptake, quadratic_flow, peri.sp) waterpotential_sr[1,r,2:] = waterpotential #TODO: this is a test #vg.fast_mfp[peri.sp](waterpotential_dumux[1,r,-1]) solutes_dumux_ff[1,r,0] = -Uptake[0] - solutes_dumux_ff[1,r,1] = -ss_uptake[0] + solutes_dumux_ff[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_dumux_ff[1,r,2:] = soluteconcentration[:] solutes_dumux_ff[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area @@ -508,12 +509,12 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) Km = 1.5e-7 #mol/cm3 -for i in range(len(solute_sr_nf_conc)): - solute_sr_nf[i]=-Vmax_per_area*solute_sr_nf_conc[i]/(Km+solute_sr_nf_conc[i]) - solute_ss_nf[i]=-Vmax_per_area*solute_ss_nf_conc[i]/(Km+solute_ss_nf_conc[i]) - solute_sr[i]=-Vmax_per_area*solute_sr_conc[i]/(Km+solute_sr_conc[i]) - solute_ss[i]=-Vmax_per_area*solute_ss_conc[i]/(Km+solute_ss_conc[i]) - solute_ff[i]=-Vmax_per_area*solute_ff_conc[i]/(Km+solute_ff_conc[i]) +#for i in range(len(solute_sr_nf_conc)): + #solute_sr_nf[i]=-Vmax_per_area*solute_sr_nf_conc[i]/(Km+solute_sr_nf_conc[i]) + #solute_ss_nf[i]=-Vmax_per_area*solute_ss_nf_conc[i]/(Km+solute_ss_nf_conc[i]) + #solute_sr[i]=-Vmax_per_area*solute_sr_conc[i]/(Km+solute_sr_conc[i]) + #solute_ss[i]=-Vmax_per_area*solute_ss_conc[i]/(Km+solute_ss_conc[i]) + #solute_ff[i]=-Vmax_per_area*solute_ff_conc[i]/(Km+solute_ff_conc[i]) #print(suptake_dumux) #print(solute_ss) diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index eb547666d..5077e6ad7 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -95,24 +95,24 @@ def open_lookup_solutes_simplified(self, filename): def open_lookup_solutes(self, filename): """opens an additional look-up table from a file to quickly find soil root solute concentrations in the steady rate case""" npzfile_sr = np.load(filename + ".npz") - Ds_, outer_mfp_, inner_mfp_ = npzfile_sr["Phi1_"], npzfile_sr["waterflow_"], npzfile_sr["wateruptake_"], npzfile_sr["r_eval_"] + Phi_2_, Phi_1_, Phi_0_, r_eval_ = npzfile_sr["Phi_2_"], npzfile_sr["Phi_1_"], npzfile_sr["Phi_0_"], npzfile_sr["r_eval_"] soil = npzfile_sr["soil"] - conc_rel_c, inflow_rel_c, Uptake_rel_c = npzfile_sr["conc_rel_c"], npzfile_sr["inflow_rel_c"], npzfile_sr["Uptake_rel_c"] - conc_mean_c, inflow_mean_c, Uptake_mean_c = npzfile_sr["conc_mean_c"], npzfile_sr["inflow_mean_c"], npzfile_sr["Uptake_mean_c"] + conc_rel_c, Uptake_rel_c, quadratic_rel_c = npzfile_sr["conc_rel_c"], npzfile_sr["Uptake_rel_c"], npzfile_sr["quadratic_rel_c"] + conc_mean_c, Uptake_mean_c, quatratic_mean_c = npzfile_sr["conc_mean_c"], npzfile_sr["Uptake_mean_c"], npzfile_sr["quatratic_mean_c"] self.lookup_table_sr_solutes = { "conc_rel_c" : [0], "conc_mean_c" : [0], - "inflow_rel_c" : [0], - "inflow_mean_c" : [0], - "Uptake_rel_c" : [0], - "Uptake_mean_c" : [0] + "Uptake_rel_c" : [0], + "Uptake_mean_c" : [0], + "quadratic_rel_c" : [0], + "quatratic_mean_c" : [0] } - self.lookup_table_sr_solutes["conc_rel_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , conc_rel_c) # method = "nearest" fill_value = None , bounds_error=False - self.lookup_table_sr_solutes["conc_mean_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , conc_mean_c) - self.lookup_table_sr_solutes["inflow_rel_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , inflow_rel_c) - self.lookup_table_sr_solutes["inflow_mean_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , inflow_mean_c) - self.lookup_table_sr_solutes["Uptake_rel_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , Uptake_rel_c) - self.lookup_table_sr_solutes["Uptake_mean_c"] = RegularGridInterpolator((Phi1_, waterflow_, wateruptake_, r_eval_) , Uptake_mean_c) + self.lookup_table_sr_solutes["conc_rel_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , conc_rel_c) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_table_sr_solutes["conc_mean_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , conc_mean_c) + self.lookup_table_sr_solutes["Uptake_rel_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , Uptake_rel_c) + self.lookup_table_sr_solutes["Uptake_mean_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , Uptake_mean_c) + self.lookup_table_sr_solutes["quadratic_rel_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , quadratic_rel_c) + self.lookup_table_sr_solutes["quatratic_mean_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , quatratic_mean_c) self.sp = vg.Parameters(soil) vg.create_mfp_lookup(self.sp) # does this have to be repeated here? @@ -284,9 +284,9 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km, Ds, sp, mode = "ff"): """ - steady rate assumption of solute uptake by roots TODO: insert citaiton + steady rate assumption of solute uptake by roots TODO: insert citation - Phi_out outer matrix flux potential [cm2/d] + Phi_out outer matrix flux potential [cm2/d] rootwateruptake radial root water uptake [cm2/d] waterinflow radial water inflow at r_prhiz [cm2/d] r_root root radius [cm] @@ -313,8 +313,7 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ rsc = np.zeros(n_segments) #conentration next to the root Uptake = np.zeros(n_segments) #total solute uptake of the root - ss_uptake = np.zeros(n_segments) #inflow into the parirhizal zone - sr_uptake = np.zeros(n_segments) #solute depletion rate of the perirhizal zone + quadratic_flow = np.zeros(n_segments) #quadratic flow into the parirhizal zone #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): @@ -344,70 +343,64 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ #TODO: implement using the lookup table here r_eval = np.logspace(np.log10(r_root[i]), np.log10(r_prhiz[i]), num = 40) - r_eval = [r / scaling / r_prhiz[i] for r in r_eval] - #Phi1 = 1 - #waterflow = 0 - #wateruptake = 0 - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) + r_eval = [r / scaling for r in r_eval] + + conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quatratic_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) + #default prefactors for the steady state case pre_c = 0 - pre_srUptake = 1 - pre_inflow = 0 + pre_Uptake = 0 + pre_quadratic = 1 absolute = 0 - #pre_c * c(r_prhiz) + pre_srUptake * srUptake + pre_inflow * inflow = absolute + #pre_c * c(r_prhiz) + pre_Uptake * Uptake + pre_quadratic * quadratic = absolute match mode: case "ss": #steady state - pre_c, pre_srUptake, pre_inflow, absolute = 0, 1, 0, 0 + pre_c, pre_Uptake, pre_quadratic, absolute = 0, 0, 1, 0 case "sr": #steady rate uptake, no influx - pre_c, pre_srUptake, pre_inflow, absolute = 0, -1, 1, 0 + pre_c, pre_Uptake, pre_quadratic, absolute = 0, r_prhiz[i]**2, -1, 0 case "ff": #far field approximation # TODO insert link to Tiina Roose publication, and current application B_abs = c_outer[i] * (r_prhiz[i]**2)/ (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) B_cprhiz = -1 * (r_prhiz[i]**2) / (math.exp(-2*r_prhiz[i]**2)-math.exp(-r_prhiz[i]**2)) - pre_inflow = 1 - pre_c = - Ds_func(r_prhiz[i]) * B_cprhiz * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 - waterinflow[i] / (2 * np.pi * r_prhiz[i]) #TODO check sign + pre_Uptake = 1 + pre_quadratic = r_prhiz[i]**2 + pre_c = Ds_func(r_prhiz[i]) * B_cprhiz * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 + waterinflow[i] / (2 * np.pi * r_prhiz[i]) absolute = Ds_func(r_prhiz[i]) * B_abs * math.exp(-r_prhiz[i]**2) / r_prhiz[i]**2 - #pre_c = - Ds_func(r_prhiz[i]) * B_cprhiz * math.exp(-r_prhiz[i]) / r_prhiz[i] - waterinflow[i] / (2 * np.pi * r_prhiz[i]) #TODO check sign - #absolute = Ds_func(r_prhiz[i]) * B_abs * math.exp(-r_prhiz[i]) / r_prhiz[i] - pre_srUptake = 1 case "dirichlet": pre_c = 1 - absolute = c_outer[i] + pre_Uptake = 0 + pre_quadratic = 0 + absolute = c_outer[i] case _: #default - pre_c, pre_srUptake, pre_inflow, absolute = 0, 1, 0, 0 + pre_c, pre_Uptake, pre_quadratic, absolute = 0, 0, 1, 0 + print("The mode of a steady rate solute flow approximation could not be identified, use steady state.") - #variables to eliminate: ss flow, sr uptake, c_prhiz, Uptake + #variables to eliminate: Uptake, quadratic, c_prhiz # variable for the final equations: rsc - A = np.zeros((4,4)) - b = np.zeros((4,2)) #right hand side of the linear equation, one for absolute values, one for the rsc value + A = np.zeros((3,3)) + b = np.zeros((3,2)) #right hand side of the linear equation, one for absolute values, one for the rsc value - #linear equations for c_prhiz, rsc, ss_flow, sr_uptake, Uptake (4 linear, 1 quadratic) + #linear equations for c_prhiz, Uptake, quadratic, rsc (3 linear, 1 quadratic) - #c11 * ss flow + c12 * sr uptake + c13 * rsc = c_mean - A[0,0] = inflow_mean_c[-1] - A[0,1] = Uptake_mean_c[-1] + #c11 * Uptake + c12 * quadratic + c13 * rsc = c_mean + A[0,0] = Uptake_mean_c[-1] + A[0,1] = quatratic_mean_c[-1] b[0,1] = -conc_mean_c[-1] b[0,0] = c_soil[i] - #c21 * ss flow + c22 * sr uptake + c23 * rsc = c_prhiz - A[1,0] = inflow_rel_c[-1] - A[1,1] = Uptake_rel_c[-1] + #c21 * Uptake + c22 * quadratic + c23 * rsc = c_prhiz + A[1,0] = Uptake_rel_c[-1] + A[1,1] = quadratic_rel_c[-1] A[1,2] = -1 b[1,1] = -conc_rel_c[-1] - #ss flow + sr uptake = Uptake - A[2,0] = 1 - #A[2,1] = 1 - A[2,1] = 0 - A[2,3] = -1 - - #pre_c * c(r_prhiz) + pre_srUptake * srUptake + pre_inflow * inflow (= ssflow) = absolute - A[3,0] = pre_inflow - A[3,1] = pre_srUptake - A[3,2] = pre_c - b[3,0] = absolute + #pre_uptake * Uptake + pre_quadratic * quadratic + pre_c * c_prhiz = absolute + A[2,0] = pre_Uptake + A[2,1] = pre_quadratic + A[2,2] = pre_c + b[2,0] = absolute #m*rsc+n = [ss uptake, sr uptake, c(rprhiz), Uptake] n = la.solve(A,b[:,0]) @@ -415,33 +408,31 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ #solve quadratic equation #Uptake = Vmax * rsc / (Km + rsc) - #(Km + rsc) * (m[3]*rsc+n[3]) = Vmax * rsc - # m[3] * rsc**2 + (Km[i]*m[3]+n[3]-Vmax[i]) * rsc + Km[i]*n[3] = 0 + #Uptake = m[0]*rsc+n[0] + #(Km + rsc) * (m[0]*rsc+n[0]) = Vmax * rsc + # m[0] * rsc**2 + (Km[i]*m[0]+n[0]-Vmax[i]) * rsc + Km[i]*n[0] = 0 tol = 1e-9 #arbitrary for now, TODO think of a reasonable tolerance - if abs(m[3]) Ds0_ref means scaling >1, the region is modeled to be smaller than it actually is scaling = Ds0 / self.Ds0_ref # Ds0 > Ds0_ref means scaling >1, the region is modeled to be smaller than it actually is - #Phi1 = Phi(1/scaling) - waterinflow * np.log(scaling) - (rootwateruptake - waterinflow) * r_prhiz**2 / (r_prhiz**2 - r_root**2) * (np.log(scaling)-1/2 * (1-1/scaling**2)) - #waterflow = radial_waterflow(scaling) / scaling - r_eval = [r / scaling / r_prhiz for r in r_eval] - #wateruptake = (rootwateruptake - waterinflow) / (r_prhiz**2 - r_root**2) / scaling**2 #* r_prhiz**2 - - Phi_2 = Phi_2 * ((scaling*r_prhiz)**2) + r_eval = [r / scaling for r in r_eval] + Phi_2 = Phi_2 * (scaling**2) Phi_1 = Phi_1 - Phi_0 = Phi_0 + Phi_1 * np.log(scaling*r_prhiz) - #radial_waterflow = lambda r : 2 * np.pi * (r**2) * Phi_2 + 2 * np.pi * Phi_1 #for the new scaling + Phi_0 = Phi_0 + Phi_1 * np.log(scaling) + #values necessary for plotting + n = len(r_eval) + conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quatratic_mean_c = np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n) + + if self.lookup_table_sr_solutes: + for i in range(n): + conc_rel_c[i] = self.lookup_table_sr_solutes["conc_rel_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + conc_mean_c[i] = self.lookup_table_sr_solutes["conc_mean_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + Uptake_rel_c[i] = self.lookup_table_sr_solutes["Uptake_rel_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + Uptake_mean_c[i] = self.lookup_table_sr_solutes["Uptake_mean_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + quadratic_rel_c[i] = self.lookup_table_sr_solutes["quadratic_rel_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + quatratic_mean_c[i] = self.lookup_table_sr_solutes["quatratic_mean_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + else: + conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quatratic_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) - Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d - Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) - Km = 1.5e-7 #mol/cm3 - - #rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([Phi_out], [rootwateruptake], [waterinflow], [r_root], [r_prhiz], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") - rsc, Uptake, ss_uptake, sr_uptake, inflow_mean_c_0 = self.soil_root_solutes_sr([Phi_out], [rootwateruptake], [waterinflow], [r_eval[0]], [r_eval[-1]], [1.98e-5], [2.0e-5], [Vmax], [Km], [Ds0], sp, mode = "ss") - conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) - #conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c = self.solute_linearequation_sr_(1, 0, 0, r_eval, sp) - #c_root = rsc - #print("compareinflow", inflow_mean_c_0, inflow_mean_c) - - soluteconcentration = c_root * conc_rel_c + ss_uptake * inflow_rel_c + sr_uptake * Uptake_rel_c - soluteconcentration_mean = c_root * conc_mean_c + ss_uptake * inflow_mean_c + sr_uptake * Uptake_mean_c - - print("output disc1", c_root, ss_uptake, sr_uptake, scaling) - print("output disc2", conc_rel_c[0], inflow_rel_c[0], Uptake_rel_c[0], soluteconcentration[0]) - print("output disc3", conc_rel_c[-1], inflow_rel_c[-1], Uptake_rel_c[-1], soluteconcentration[-1]) - print("output disc4", conc_mean_c[-1], inflow_mean_c[-1], Uptake_mean_c[-1], soluteconcentration_mean[-1]) + soluteconcentration = c_root * conc_rel_c + Uptake * Uptake_rel_c + quadratic_flow * quadratic_rel_c + soluteconcentration_mean = c_root * conc_mean_c + Uptake * Uptake_mean_c + quadratic_flow * quatratic_mean_c @@ -578,8 +564,6 @@ def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): inflow_mean_c[1:] = np.array([np.average(inflow_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) Uptake_mean_c[1:] = np.array([np.average(Uptake_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) - print("Phis", Phi_0, Phi_1, Phi_2) - return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c @@ -664,7 +648,8 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r if p<0: rsc[i]=-p/2-math.sqrt(pow(p/2,2)-q) else: - rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) + rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) + print("concentration sr nf f", rsc[i], -p/2-math.sqrt(pow(p/2,2)-q), -p/2+math.sqrt(pow(p/2,2)-q)) return rsc From 371516455c04863ca27e22cf4e87cffdc5fc3f39 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Thu, 9 Jul 2026 16:32:30 +0200 Subject: [PATCH 36/41] implemented test script for Perirhizal solute flow --- .../test_perirhizal.py | 567 +++++++++++------- src/functional/Perirhizal.py | 240 +++++--- 2 files changed, 501 insertions(+), 306 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index 687c49db3..dc11dc31e 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -38,15 +38,15 @@ do_computation = True #should the computation be run or take the data from a saved file # general parameters -max_time = 1.0 # d -n_times = 100 # number of time intervals -r_prhiz = 0.6 # perirhizal radius[cm] +max_time = 10 # d +n_times = 1000 # number of time intervals +r_prhiz = 0.5 # perirhizal radius[cm], computed for a RLD above 1cm/cm3 r_root = 0.02 # root radius [cm] -NC = 30 # number of spatial discretisations -length = 1 #default length of the segment, will not change the outcpme as all variables are constant in this direction [cm] +NC = 40 # number of spatial discretisations +length = 1 #default length of the segment, will not change the outcpme as all variables are assumed constant in this direction [cm] -#two scenarios will be computed: one without inflow, another with a Dirichlet BC -n_scenarios = 2 +#three scenarios will be computed: one without inflow, one with an advective flow (Dirichlet BC), another with a Dirichlet BC +n_scenarios = 3 #initial conditions initial_waterpotential = -200 @@ -64,16 +64,20 @@ #solute outputs #simulations in dumux -solutes_dumux = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm d) +solutes_dumux = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) #analytical approximations #steady state approximations -solutes_dumux_ss = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm d) +solutes_ss = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) +#simple steady rate approximations +solutes_sr_simp = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) #steady rate approximations -solutes_dumux_sr = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm d) -#general steady rate with far field approximation -solutes_dumux_ff = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm d) - - +solutes_sr = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) +#general steady rate with far field approximation, not used here as there is no scenario for it +#solutes_ff = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) +#dirichlet outer BC +solutes_d = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) +#assume a uniform solute concentration +solutes_u = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) #discretisation lb = 0.5 @@ -92,9 +96,12 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu watercontent_sr = np.zeros((n_scenarios, n_times, NC+2)) #watercontent in cm3/cm3, (radial) uptake and inflow in cm/d waterpotential_sr = np.zeros((n_scenarios, n_times, NC+2)) #waterpotential in cm, (radial) uptake and inflow in cm/d solutes_dumux = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) - solutes_dumux_sr = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) - solutes_dumux_ss = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) - solutes_dumux_ff = np.zeros((n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, (radial) uptake and inflow in mol/(cm2d) + solutes_ss = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) + solutes_sr_simp = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) + solutes_sr = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) + solutes_ff = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) + solutes_d = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) + solutes_u = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) simtimes = np.linspace(0,max_time,n_times+1)[1:] dt = max_time / n_times @@ -103,13 +110,6 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #soil parameters soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil - #discretisation - #lb = 0.5 - #points = np.logspace(np.log(r_root) / np.log(lb), np.log(r_prhiz) / np.log(lb), NC+1, base = lb) - #CC = np.array([(points[i] + points[i+1])/2 for i in range(NC)]) - #volumes = np.array([(points[i+1]**2 - points[i]**2)*3.14 for i in range(NC)]) - - # root conductivity and solute uptake parameters, constant throughout the entire simulation time molarMassWater = 18 #g/mol molarMassSolute = 62 #g/mol @@ -117,8 +117,8 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu inner_kr = root_conductivity * r_root * 2 * 3.14 waterdemand = -0.05 #cm/d radial_waterdemand = 2*3.14*r_root * waterdemand #cm2/d - Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d - Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) + Vmax = 4.0e-11 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / (cm d) + Vmax_per_area = Vmax / (2*3.14*r_root) #mol / (cm d) /cm2 = mol/(cm2d) Km = 1.5e-7 #mol/cm3 #diffusion coefficient of nitrate @@ -136,9 +136,10 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu # initialise the dumux models for the scenarios s_nf = RichardsWrapper(RichardsNCCylFoam()) #no flux outer BC - s_g = RichardsWrapper(RichardsNCCylFoam()) #Cauchy outer BC + s_af = RichardsWrapper(RichardsNCCylFoam()) #dirichlet BC for water, advective flow for solutes + s_d = RichardsWrapper(RichardsNCCylFoam()) #Dirichlet outer BC - for s in [s_nf, s_g]: + for s in [s_nf, s_af, s_d]: s.initialize() s.createGrid1d(points, length = length/100) # [m] -> [cm] s.setVGParameters([soilVG]) @@ -149,33 +150,59 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu s.setTopBC("constantFluxCyl",0.0) # [cm/day] "noFlux")#default, will be changed for one scenario s.setParameter("Soil.BC.Top.SType", "3") # constantFluxCyl=3 (SType = Solute Type) s.setParameter("Soil.BC.Top.CValue", "0.0") - else: - s.setTopBC("constantPressure",initial_waterpotential) - s.setParameter("Soil.BC.Top.SType", "10") #advective flow - s.setParameter("Soil.BC.Top.CValue", str(initial_soluteconcentration*molarMassSolute)) + else: + if s == af: + s.setParameter("Soil.BC.Top.Type", "11") #dummy Dirichlet BC so dumux does not get mad + s.setParameter("Soil.BC.Top.Value", str(initial_waterpotential)) + s.setParameter("Soil.BC.Top.SType", "10") #advective flow + s.setParameter("Soil.BC.Top.CValue", str(0)) + else: + s.setParameter("Soil.BC.Top.Type", "11") #dummy Dirichlet BC so dumux does not get mad + s.setParameter("Soil.BC.Top.Value", str(initial_waterpotential)) + s.setParameter("Soil.BC.Top.SType", "11") #dummy Dirichlet BC so dumux does not get mad + s.setParameter("Soil.BC.Top.CValue", str(initial_soluteconcentration*molarMassSolute)) s.setParameter("Soil.BC.Bot.SType", "8") # michaelisMenten=8 (SType = Solute Type) s.setParameter("Soil.BC.Bot.CValue", "0.0") #should not matter - #s.setParameter("RootSystem.Uptake.Vmax", str(Vmax_per_area*molarMassSolute)) #mol/(cm2d) -> g/(cm2 d) - s.setParameter("RootSystem.Uptake.Vmax", str(Vmax*molarMassSolute)) #mol/d -> g/d #TODO: Vmax or Vmax per area? + s.setParameter("RootSystem.Uptake.Vmax", str(Vmax_per_area*molarMassSolute)) #mol/(cm2d) -> g/(cm2 d) + #s.setParameter("RootSystem.Uptake.Vmax", str(Vmax*molarMassSolute)) #mol/d -> g/d #TODO: Vmax or Vmax per area? s.setParameter("RootSystem.Uptake.Km", str(Km*molarMassSolute)) # mol/cm3 -> g/cm3 - "RootSystem.Uptake.Vmax" - "RootSystem.Uptake.Km" - s.setParameter("Soil.IC.C", str(initial_soluteconcentration*molarMassSolute)) # g / cm3 # TODO specialised setter? + s.setParameter("Soil.IC.C", str(initial_soluteconcentration*molarMassSolute)) # g / cm3 s.setParameter("Component.MolarMass", str(molarMassWater/1000)) #g/mol -> kg/mol water s.setParameter("1.Component.MolarMass", str(molarMassSolute/1000)) #g/mol -> kg/mol nitrate s.setParameter("1.Component.LiquidDiffusionCoefficient", str(Ds / 1.e4 / (24*3600))) #cm^2/s -> m^2/s s.initializeProblem(maxDt = 0.01) s.ddt = 1.e-4 # days - #in the general case the outer conditions are kept constant - #s_g.setTopBC("constantPressure",initial_waterpotential) - #s_g.setParameter("Soil.BC.Top.SType", "1") #Dirichlet BC - #s_g.setParameter("Soil.BC.Top.SType", "10") #advective flow - #s_g.setParameter("Soil.BC.Top.CValue", str(initial_soluteconcentration*molarMassSolute)) - - cellVolumes = s_g.getCellSurfacesCyl() * length # cm3 + #initial solute concentration for the analytical approximations + #simplified steady rate no flux (1d lookup table) + solutes_sr_simp = np.zeros((n_scenarios, n_times, NC+2)) + solutes_sr_simp[0,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_sr_simp[1,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_sr_simp[2,0,2:]=np.ones(NC) * initial_soluteconcentration + + #steady state solute flow + solutes_ss = np.zeros((n_scenarios, n_times, NC+2)) + solutes_ss[0,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_ss[1,0,2:]=np.ones(NC) * initial_soluteconcentration + + #steady rate no flux outer BC solute flow in the general water flow + solutes_sr = np.zeros((n_scenarios, n_times, NC+2)) + solutes_sr[0,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_sr[1,0,2:]=np.ones(NC) * initial_soluteconcentration + + #Dirichlet BC + solutes_d = np.zeros((n_scenarios, n_times, NC+2)) + solutes_d[0,0,2:]=np.ones(NC) * initial_soluteconcentration + + #uniform concentration (= no analytical approximation) + solutes_u = np.zeros((n_scenarios, n_times, NC+2)) + solutes_u[0,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_u[1,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_u[2,0,2:]=np.ones(NC) * initial_soluteconcentration + + #variables for the no flux case mean_watercontent_nf = 0.1 #cm3/cm3 mean_waterpotential_nf = -100 #cm @@ -186,66 +213,94 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu Phi_outer_nf = 1.0 #mfp at the outer perirhizal radius rootuptake_w_nf = 1.0 #water uptake of the root, cm / d inflow_w_nf = 0.0 #water uptake from outside the perirhizal zone, cm/d - rootuptake_s_nf = 1.0 #water uptake of the root, mol / cm2d - inflow_s_nf = 0.0 #water uptake from outside the perirhizal zone, mol / cm2d + rootuptake_s_nf = 0.0 #solute uptake of the root, mol / cm2d + inflow_s_nf = 0.0 #solute uptake from outside the perirhizal zone, mol / cm2d #variables for the general case - mean_watercontent_g = 0.1 #cm3/cm3 - mean_waterpotential_g = -100 #cm - mean_solutecontent_g = 0 #mol/cm3 - Phi_soil_g = 1.0 #mfp of the mean soil - Phi_root_g = 1.0 #mfp next to the root + mean_watercontent_af = 0.1 #cm3/cm3 + mean_waterpotential_af = -100 #cm + mean_solutecontent_af = 0 #mol/cm3 + Phi_soil_af = 1.0 #mfp of the mean soil + Phi_root_af = 1.0 #mfp next to the root #mfp parameters for the perirhizal model - Phi_outer_g = 1.0 #mfp at the outer perirhizal radius - rootuptake_w_g = 1.0 #water uptake of the root, cm / d - inflow_w_g = 0.0 #water uptake from outside the perirhizal zone, cm/d - rootuptake_s_g = 1.0 #water uptake of the root, mol / cm2d - inflow_s_g = 0.0 #water uptake from outside the perirhizal zone, mol / cm2d + Phi_outer_af = 1.0 #mfp at the outer perirhizal radius + rootuptake_w_af = 1.0 #water uptake of the root, cm / d + inflow_w_af = 0.0 #water uptake from outside the perirhizal zone, cm/d + rootuptake_s_af = 0.0 #solute uptake of the root, mol / cm2d + inflow_s_af = 0.0 #solute uptake from outside the perirhizal zone, mol / cm2d + #variables for the Dirichlet case + mean_watercontent_d = 0.1 #cm3/cm3 + mean_waterpotential_d = -100 #cm + mean_solutecontent_d = 0 #mol/cm3 + Phi_soil_d = 1.0 #mfp of the mean soil + Phi_root_d = 1.0 #mfp next to the root + #mfp parameters for the perirhizal model + Phi_outer_d = 1.0 #mfp at the outer perirhizal radius + rootuptake_w_d = 1.0 #water uptake of the root, cm / d + inflow_w_d = 0.0 #water uptake from outside the perirhizal zone, cm/d + rootuptake_s_d = 0.0 #solute uptake of the root, mol / cm2d + inflow_s_d = 0.0 #solute uptake from outside the perirhizal zone, mol / cm2d - for r, time in enumerate(simtimes): + + for r in range(1,len(simtimes)): - dt = time + dt = time[r] if r>0: - dt = time - simtimes[r-1] + dt = time[r] - simtimes[r-1] print('time',time) print('no flux BC') print("*****", "#", r, "external time step", dt, " d, simulation time", s_nf.simTime, "d, internal time step", s_nf.ddt, "d") - print('general') - print("*****", "#", r, "external time step", dt, " d, simulation time", s_g.simTime, "d, internal time step", s_g.ddt, "d") + print('advective flow') + print("*****", "#", r, "external time step", dt, " d, simulation time", s_af.simTime, "d, internal time step", s_af.ddt, "d") + print('Dirichlet BC') + print("*****", "#", r, "external time step", dt, " d, simulation time", s_d.simTime, "d, internal time step", s_d.ddt, "d") #one timestep s_nf.solve(dt, saveInnerFluxes_ = True) - s_g.solve(dt, saveInnerFluxes_ = True) + s_af.solve(dt, saveInnerFluxes_ = True) + s_d.solve(dt, saveInnerFluxes_ = True) #watercontent and solute content, discretised watercontent_nf = s_nf.getWaterContent() # cm3 waterpotential_nf = np.array([vg.pressure_head(watercontent_nf[i],peri.sp) for i in range(NC)]) # cm - watercontent_g = s_g.getWaterContent() # cm3 - waterpotential_g = np.array([vg.pressure_head(watercontent_g[i],peri.sp) for i in range(NC)]) # cm + watercontent_af = s_af.getWaterContent() # cm3 + waterpotential_af = np.array([vg.pressure_head(watercontent_g[i],peri.sp) for i in range(NC)]) # cm + watercontent_d = s_d.getWaterContent() # cm3 + waterpotential_d = np.array([vg.pressure_head(watercontent_g[i],peri.sp) for i in range(NC)]) # cm solutes_nf = s_nf.getSolution(1) / molarMassSolute # mol/cm3 - solutes_g = s_g.getSolution(1) / molarMassSolute # mol/cm3 + solutes_af = s_af.getSolution(1) / molarMassSolute # mol/cm3 + solutes_d = s_d.getSolution(1) / molarMassSolute # mol/cm3 #inflow and outflow rootuptake_w_nf = s_nf.getInnerFlow(0, length) /(length*(2*np.pi*r_root)) # cm /d - rootuptake_s_nf = s_nf.getInnerFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_root)) # mol / cm2d + rootuptake_s_nf = s_nf.getInnerFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_root)) # mol / (cm2d) inflow_w_nf = s_nf.getOuterFlow(0, length) /(length*(2*np.pi*r_prhiz)) # cm /d - inflow_s_nf = s_nf.getOuterFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_prhiz)) # mol / cm2d + inflow_s_nf = s_nf.getOuterFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_prhiz)) # mol / (cm2d) + + rootuptake_w_af = s_af.getInnerFlow(0, length) /(length*(2*np.pi*r_root)) # cm /d + rootuptake_s_af = s_af.getInnerFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_root)) # mol / (cm2d) + inflow_w_af = s_af.getOuterFlow(0, length) /(length*(2*np.pi*r_prhiz)) # cm /d + inflow_s_af = s_af.getOuterFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_prhiz)) # mol / (cm2d) - rootuptake_w_g = s_g.getInnerFlow(0, length) /(length*(2*np.pi*r_root)) # cm /d - rootuptake_s_g = s_g.getInnerFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_root)) # mol / cm2d - inflow_w_g = s_g.getOuterFlow(0, length) /(length*(2*np.pi*r_prhiz)) # cm /d - inflow_s_g = s_g.getOuterFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_prhiz)) # mol / cm2d + rootuptake_w_d = s_d.getInnerFlow(0, length) /(length*(2*np.pi*r_root)) # cm /d + rootuptake_s_d = s_d.getInnerFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_root)) # mol / (cm2d) + inflow_w_d = s_d.getOuterFlow(0, length) /(length*(2*np.pi*r_prhiz)) # cm /d + inflow_s_d = s_d.getOuterFlow(1, length) / molarMassSolute /(length*(2*np.pi*r_prhiz)) # mol / (cm2d) rootuptake_w_nf = rootuptake_w_nf[0] rootuptake_s_nf = rootuptake_s_nf[0] inflow_w_nf = inflow_w_nf[0] inflow_s_nf = inflow_s_nf[0] - rootuptake_w_g = rootuptake_w_g[0] - rootuptake_s_g = rootuptake_s_g[0] - inflow_w_g = abs(inflow_w_g[0]) - inflow_s_g = abs(inflow_s_g[0]) #TODO take all the flows to the root as negative? + rootuptake_w_af = rootuptake_w_af[0] + rootuptake_s_af = rootuptake_s_af[0] + inflow_w_af = inflow_w_af[0] + inflow_s_af = inflow_s_af[0] + rootuptake_w_d = rootuptake_w_d[0] + rootuptake_s_d = rootuptake_s_d[0] + inflow_w_d = inflow_w_d[0] + inflow_s_d = inflow_s_d[0] #store the dumux outputs #scenario no flux outer BC @@ -258,26 +313,43 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_dumux[0,r,0]=rootuptake_s_nf solutes_dumux[0,r,1]=inflow_s_nf solutes_dumux[0,r,2:]=solutes_nf - #general scenario - watercontent_dumux[1,r,0]=rootuptake_w_g - watercontent_dumux[1,r,1]=inflow_w_g - watercontent_dumux[1,r,2:]=watercontent_g - waterpotential_dumux[1,r,0]=rootuptake_w_g - waterpotential_dumux[1,r,1]=inflow_w_g - waterpotential_dumux[1,r,2:]=waterpotential_g - solutes_dumux[1,r,0]=rootuptake_s_g - solutes_dumux[1,r,1]=inflow_s_g - solutes_dumux[1,r,2:]=solutes_g + #advective flow scenario + watercontent_dumux[1,r,0]=rootuptake_w_af + watercontent_dumux[1,r,1]=inflow_w_af + watercontent_dumux[1,r,2:]=watercontent_af + waterpotential_dumux[1,r,0]=rootuptake_w_af + waterpotential_dumux[1,r,1]=inflow_w_af + waterpotential_dumux[1,r,2:]=waterpotential_af + solutes_dumux[1,r,0]=rootuptake_s_af + solutes_dumux[1,r,1]=inflow_s_af + solutes_dumux[1,r,2:]=solutes_af + #Dirichlet BC + watercontent_dumux[1,r,0]=rootuptake_w_d + watercontent_dumux[1,r,1]=inflow_w_d + watercontent_dumux[1,r,2:]=watercontent_d + waterpotential_dumux[1,r,0]=rootuptake_w_d + waterpotential_dumux[1,r,1]=inflow_w_d + waterpotential_dumux[1,r,2:]=waterpotential_d + solutes_dumux[1,r,0]=rootuptake_s_d + solutes_dumux[1,r,1]=inflow_s_d + solutes_dumux[1,r,2:]=solutes_d #determine means #no flux outer BC mean_watercontent_nf = np.average(watercontent_nf, weights=volumes) mean_waterpotential_nf = vg.pressure_head(mean_watercontent_nf, peri.sp) mean_soluteconcent_nf = np.average(solutes_nf, weights=np.multiply(mean_watercontent_nf, volumes)) - #general outer BC - mean_watercontent_g = np.average(watercontent_g, weights=volumes) - mean_waterpotential_g = vg.pressure_head(mean_watercontent_g, peri.sp) - mean_soluteconcent_g = np.average(solutes_g, weights=np.multiply(mean_watercontent_g, volumes)) + mean_soluteconcent_sr_simp_nf = np.average(solutes_sr_simp[0,r,2:], weights=np.multiply(mean_watercontent_nf, volumes)) + #advective flow + mean_watercontent_af = np.average(watercontent_af, weights=volumes) + mean_waterpotential_af = vg.pressure_head(mean_watercontent_af, peri.sp) + mean_soluteconcent_af = np.average(solutes_af, weights=np.multiply(mean_watercontent_af, volumes)) + mean_soluteconcent_sr_simp_af = np.average(solutes_sr_simp[1,r,2:], weights=np.multiply(mean_watercontent_af, volumes)) + #Dirichlet BC + mean_watercontent_d = np.average(watercontent_d, weights=volumes) + mean_waterpotential_d = vg.pressure_head(mean_watercontent_d, peri.sp) + mean_soluteconcent_d = np.average(solutes_d, weights=np.multiply(mean_watercontent_d, volumes)) + mean_soluteconcent_sr_simp_d = np.average(solutes_sr_simp[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes)) #determine coefficients for the analytical approximation #equation [4] in Schroeder2008 doi:10.2136/vzj2007.0114 @@ -289,12 +361,16 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu Phi_outer_nf = Phi_soil_nf - (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_nf*r_prhiz)*r_prhiz*np.log(0.53) #mfp at the outer perirhizal radius Phi_nf = lambda r: Phi_outer_nf + (rootuptake_w_nf*r_root-inflow_w_nf*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_nf*r_prhiz)*r_prhiz*np.log(r/r_prhiz)#mfp function depending on radius Phi_root_nf = Phi_nf(r_root)#mfp next to the root - #general outer BC: Dirichlet BC to a fixed potential - Phi_soil_g = vg.fast_mfp[peri.sp](mean_waterpotential_g) #mfp of the mean soil - Phi_outer_g = Phi_soil_g - (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_g*r_prhiz)*np.log(0.53) #mfp at the outer perirhizal radius - Phi_g = lambda r: Phi_outer_g + (rootuptake_w_g*r_root-inflow_w_g*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_g*r_prhiz)*np.log(r/r_prhiz)#mfp function depending on radius - Phi_root_g = Phi_nf(r_root)#mfp next to the root - print("inflow", inflow_w_g) + #advective flow, Dirichlet for water transport + Phi_soil_af = vg.fast_mfp[peri.sp](mean_waterpotential_af) #mfp of the mean soil + Phi_outer_af = Phi_soil_af - (rootuptake_w_af*r_root-inflow_w_af*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_af*r_prhiz)*np.log(0.53) #mfp at the outer perirhizal radius + Phi_af = lambda r: Phi_outer_af + (rootuptake_w_af*r_root-inflow_w_af*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_af*r_prhiz)*np.log(r/r_prhiz)#mfp function depending on radius + Phi_root_af = Phi_af(r_root)#mfp next to the root + #Dirichlet BC + Phi_soil_d = vg.fast_mfp[peri.sp](mean_waterpotential_d) #mfp of the mean soil + Phi_outer_d = Phi_soil_d - (rootuptake_w_d*r_root-inflow_w_d*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_d*r_prhiz)*np.log(0.53) #mfp at the outer perirhizal radius + Phi_d = lambda r: Phi_outer_d + (rootuptake_w_d*r_root-inflow_w_d*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_d*r_prhiz)*np.log(r/r_prhiz)#mfp function depending on radius + Phi_root_d = Phi_nf(r_root)#mfp next to the root #write the steady rate approximations for the water as outputs waterpotential_sr[0,r,0] = rootuptake_w_nf @@ -304,89 +380,121 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu watercontent_sr[0,r,1] = inflow_w_nf watercontent_sr[0,r,2:] = np.array([vg.water_content(waterpotential_sr[0,r,2+i],peri.sp) for i in range(NC)]) - waterpotential_sr[1,r,0] = rootuptake_w_g - waterpotential_sr[1,r,1] = inflow_w_g - waterpotential_sr[1,r,2:] = np.array([vg.fast_imfp[peri.sp](Phi_g(CC[i])) for i in range(NC)]) - watercontent_sr[1,r,0] = rootuptake_w_g - watercontent_sr[1,r,1] = inflow_w_g + waterpotential_sr[1,r,0] = rootuptake_w_af + waterpotential_sr[1,r,1] = inflow_w_af + waterpotential_sr[1,r,2:] = np.array([vg.fast_imfp[peri.sp](Phi_af(CC[i])) for i in range(NC)]) + watercontent_sr[1,r,0] = rootuptake_w_af + watercontent_sr[1,r,1] = inflow_w_af watercontent_sr[1,r,2:] = np.array([vg.water_content(waterpotential_sr[1,r,2+i],peri.sp) for i in range(NC)]) + waterpotential_sr[2,r,0] = rootuptake_w_d + waterpotential_sr[2,r,1] = inflow_w_d + waterpotential_sr[2,r,2:] = np.array([vg.fast_imfp[peri.sp](Phi_d(CC[i])) for i in range(NC)]) + watercontent_sr[2,r,0] = rootuptake_w_d + watercontent_sr[2,r,1] = inflow_w_d + watercontent_sr[2,r,2:] = np.array([vg.water_content(waterpotential_sr[2,r,2+i],peri.sp) for i in range(NC)]) + #compute the analytical approximations for the solute uptake #case of dumux no flux outer BC - #use the outer concentration for the steady state approximation here, even if that is not necessarily known in the macroscopic model - #(the outer concentration can be computed as a mean of all surrounding voxels? - #TODO: check weather Vmax or Vmax per area - #result_solutes_ss_nf = peri.soil_root_solutes_ss_([Phi_root_nf], [Phi_outer_nf], [solutes_nf[-1]], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) - #result_solutes_sr_nf = peri.soil_root_solutes_sr_([Phi_root_nf], [Phi_outer_nf], [rho], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [radial_waterdemand], peri.sp) - - result_solutes_ss_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [-abs(waterdemand)], peri.sp, n_approx = 5) - result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [-abs(radial_waterdemand)], peri.sp, n_approx = 1) + result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_nf], [Vmax_per_area], [Km], Ds, [-waterdemand], peri.sp, n_approx = 10) + result_solutes_sr_af = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_af], [Phi_soil_af], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_af], [Vmax_per_area], [Km], Ds, [-waterdemand], peri.sp, n_approx = 10) + result_solutes_sr_d = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_d], [Phi_soil_d], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_d], [Vmax_per_area], [Km], Ds, [-waterdemand], peri.sp, n_approx = 10) #safe the results #(here there is no computed inflow, so index 1 doesn't do anything) - result_solutes_ss_nf = result_solutes_ss_nf[0] - solutes_dumux_ss[0,r,0]=-Vmax_per_area * result_solutes_ss_nf / (Km + result_solutes_ss_nf) result_solutes_sr_nf = result_solutes_sr_nf[0] - solutes_dumux_sr[0,r,0]=-Vmax_per_area * result_solutes_sr_nf / (Km + result_solutes_sr_nf) - print("concentration sr nf ", result_solutes_ss_nf, Km, solutes_dumux_ss[0,r,0]) + solutes_sr_simp[0,r,0]=-Vmax_per_area * result_solutes_sr_nf / (Km + result_solutes_sr_nf) + result_solutes_sr_af = result_solutes_sr_af[0] + solutes_sr_simp[1,r,0]=-Vmax_per_area * result_solutes_sr_af / (Km + result_solutes_sr_af) + result_solutes_sr_d = result_solutes_sr_d[0] + solutes_sr_simp[2,r,0]=-Vmax_per_area * result_solutes_sr_d / (Km + result_solutes_sr_d) F0_nf = peri.integral_AdvectionDiffusion_(Phi_root_nf,peri.sp) - F0_g = peri.integral_AdvectionDiffusion_(Phi_root_g,peri.sp) + F0_af = peri.integral_AdvectionDiffusion_(Phi_root_af,peri.sp) + F0_d = peri.integral_AdvectionDiffusion_(Phi_root_d,peri.sp) D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) + # the ratio of waterflow and soluteflow is assumed to remain constant throughout the perirhizal zone for j in range(NC): r_current = CC[j] F_nf = peri.integral_AdvectionDiffusion_(Phi_nf(r_current),peri.sp)-F0_nf + F_af = peri.integral_AdvectionDiffusion_(Phi_af(r_current),peri.sp)-F0_af F_g = peri.integral_AdvectionDiffusion_(Phi_g(r_current),peri.sp)-F0_g F_tilde_nf=math.exp(D_tilde*F_nf) - F_tilde_g=math.exp(D_tilde*F_g) - solutes_dumux_ss[0,r,2+j] = result_solutes_ss_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_ss[0,r,0] / waterdemand#waterdemand is assumed to be negative #TODO: look at the signs - solutes_dumux_sr[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_dumux_sr[0,r,0] / radial_waterdemand #an uptake of both water and solute is assumed + F_tilde_af=math.exp(D_tilde*F_af) + F_tilde_d=math.exp(D_tilde*F_d) + solutes_sr_simp[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_sr_simp[0,r,0] / waterdemand + solutes_sr_simp[1,r,2+j] = result_solutes_sr_af * F_tilde_af - (1-F_tilde_af) * solutes_sr_simp[1,r,0] / waterdemand + solutes_sr_simp[2,r,2+j] = result_solutes_sr_d * F_tilde_d - (1-F_tilde_d) * solutes_sr_simp[2,r,0] / waterdemand + + + # case of general steady rate water uptake + # safe the means, they are computed via the explicit Euler timestepping scheme + mean_soluteconcent_ss_af = np.average(solutes_ss[1,r,2:], weights=np.multiply(mean_watercontent_af, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_ss[1,r,0]*dt #TODO: add inflow from outside + mean_soluteconcent_ss_d = np.average(solutes_ss[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_ss[2,r,0]*dt + mean_soluteconcent_sr_af = np.average(solutes_sr[1,r,2:], weights=np.multiply(mean_watercontent_af, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_sr[1,r,0]*dt + mean_soluteconcent_sr_d = np.average(solutes_sr[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_sr[2,r,0]*dt + mean_soluteconcent_d_d = np.average(solutes_d[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_d[2,r,0]*dt + mean_soluteconcent_u_nf = np.average(solutes_u[0,r,2:], weights=np.multiply(mean_watercontent_nf, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_u[0,r,0]*dt + mean_soluteconcent_u_af = np.average(solutes_u[1,r,2:], weights=np.multiply(mean_watercontent_af, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_u[1,r,0]*dt + mean_soluteconcent_u_d = np.average(solutes_u[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_u[2,r,0]*dt - #case of general steady rate water uptake #for the steady state take again the outer concentration - #TODO: check weather Vmax or Vmax per area - rsc, Uptake, quadratic_flow = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, Uptake, quadratic_flow, peri.sp) - solutes_dumux_ss[1,r,0] = -Uptake[0] + #general steady state + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_af], [rootuptake_w_af*2*np.pi*r_root], [inflow_w_af*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_ss_af], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_dumux_ss[1,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) solutes_dumux_ss[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_dumux_ss[1,r,2:] = soluteconcentration[:] - solutes_dumux_ss[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area - #steady rate no flux outer BC - rsc, Uptake, quadratic_flow = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") - _, _, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, rootuptake_w_g*r_root, inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, Uptake, quadratic_flow, peri.sp) - solutes_dumux_sr[1,r,0] = -Uptake[0] + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_ss_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_dumux_ss[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + solutes_dumux_ss[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_dumux_ss[2,r,2:] = soluteconcentration[:] + #general steady rate no flux outer BC + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_af], [rootuptake_w_af*2*np.pi*r_root], [inflow_w_af*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_sr_af], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_dumux_sr[1,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) solutes_dumux_sr[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_dumux_sr[1,r,2:] = soluteconcentration[:] - solutes_dumux_sr[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area - #steady rate solute uptake with the farfield approximation - rsc, Uptake, quadratic_flow = peri.soil_root_solutes_sr([Phi_outer_g], [rootuptake_w_g*2*np.pi*r_root], [inflow_w_g*2*np.pi*r_prhiz], [r_root], [r_prhiz], [mean_soluteconcent_g], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "dirichlet") #doublecheck Vmax and Vmax per area - _, waterpotential, soluteconcentration = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, r_root, r_prhiz, CC, rsc, Ds, Uptake, quadratic_flow, peri.sp) - waterpotential_sr[1,r,2:] = waterpotential #TODO: this is a test #vg.fast_mfp[peri.sp](waterpotential_dumux[1,r,-1]) - solutes_dumux_ff[1,r,0] = -Uptake[0] - solutes_dumux_ff[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) - solutes_dumux_ff[1,r,2:] = soluteconcentration[:] - solutes_dumux_ff[1,r,0] = -Vmax_per_area * soluteconcentration[0] / (Km + soluteconcentration[0]) #TODO: check weather Vmax or Vmax per area + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_sr_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_dumux_sr[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + solutes_dumux_sr[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_dumux_sr[2,r,2:] = soluteconcentration[:] + #Dirichlet BC + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_d_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "dirichlet") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_dumux_d[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + solutes_dumux_d[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_dumux_d[2,r,2:] = soluteconcentration[:] + #uniform concentration + solutes_dumux_u[0,r,0] = -Vmax_per_area*mean_soluteconcent_u_nf/(Km + mean_soluteconcent_u_nf) + solutes_dumux_u[1,r,0] = -Vmax_per_area*mean_soluteconcent_u_af/(Km + mean_soluteconcent_u_af) + solutes_dumux_u[2,r,0] = -Vmax_per_area*mean_soluteconcent_u_d/(Km + mean_soluteconcent_u_d) + solutes_dumux_u[0,r,2:] = np.ones(NC) * mean_soluteconcent_u_nf + solutes_dumux_u[1,r,2:] = np.ones(NC) * mean_soluteconcent_u_af + solutes_dumux_u[2,r,2:] = np.ones(NC) * mean_soluteconcent_u_d - return watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_dumux_sr, solutes_dumux_ss, solutes_dumux_ff - + return watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_sr_simp, solutes_ss, solutes_sr, solutes_d, solutes_u if do_computation: # save everything in the np arrays for i in range(n_tests): - watercontent_dumux[i,:,:,:], waterpotential_dumux[i,:,:,:], watercontent_sr[i,:,:,:], waterpotential_sr[i,:,:,:], solutes_dumux[i,:,:,:], solutes_dumux_sr[i,:,:,:], solutes_dumux_ss[i,:,:,:], solutes_dumux_ff[i,:,:,:] = run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volumes, length, n_scenarios, initial_waterpotential, initial_soluteconcentration) - + watercontent_dumux[i,:,:,:], waterpotential_dumux[i,:,:,:], watercontent_sr[i,:,:,:], waterpotential_sr[i,:,:,:], solutes_dumux[i,:,:,:], solutes_sr_simp[i,:,:,:], solutes_ss[i,:,:,:], solutes_sr[i,:,:,:], solutes_d[i,:,:,:], solutes_u[i,:,:,:] = run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volumes, length, n_scenarios, initial_waterpotential, initial_soluteconcentration) np.savez("test_perirhizal.npz", watercontent_dumux=watercontent_dumux, waterpotential_dumux=waterpotential_dumux, watercontent_sr=watercontent_sr, waterpotential_sr=waterpotential_sr, solutes_dumux=solutes_dumux, - solutes_dumux_sr=solutes_dumux_sr, - solutes_dumux_ss=solutes_dumux_ss, - solutes_dumux_ff=solutes_dumux_ff) + solutes_sr_simp=solutes_sr_simp, + solutes_ss=solutes_ss, + solutes_sr=solutes_sr, + solutes_d=solutes_d, + solutes_u=solutes_u) else: simulation_results = np.load("test_perirhizal.npz") @@ -395,16 +503,17 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu watercontent_sr = simulation_results["watercontent_sr"] waterpotential_sr = simulation_results["waterpotential_sr"] solutes_dumux = simulation_results["solutes_dumux"] - solutes_dumux_sr = simulation_results["solutes_dumux_sr"] - solutes_dumux_ss = simulation_results["solutes_dumux_ss"] - solutes_dumux_ff = simulation_results["solutes_dumux_ff"] + solutes_sr_simp = simulation_results["solutes_sr_simp"] + solutes_ss = simulation_results["solutes_ss"] + solutes_sr = simulation_results["solutes_sr"] + solutes_d = simulation_results["solutes_d"] + solutes_u = simulation_results["solutes_u"] # compare both for the differint means of water / solute content run = 0 -#timestep = np.array([1,3,5,7,9]) timestep = np.array(np.linspace(1,9,num=5)) for i in range(5): timestep[i] = int(n_times * timestep[i] / 10) @@ -416,13 +525,11 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu linestyle_steadyrate = "dashed" linestyle_special = "dashdot" -#peri = PerirhizalPython() -#sp = vg.Parameters(soilVG) -#peri.set_soil(sp) -fig, ax1 = figure_style.subplots12(nrows=5, ncols=2) -# dumux(both) -# left: sr no flux, ss for the no flux +fig, ax1 = figure_style.subplots12(nrows=5, ncols=3) +# dumux(all 3) +# left: sr no flux +# middle: dirichlet BC water, advective flow solutes # right: ss, sr, farfield for sr waterflow @@ -430,37 +537,62 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu for i in range(5): ax2_0 = ax1[i,0].twinx() ax2_1 = ax1[i,1].twinx() + ax2_2 = ax1[i,1].twinx() #load data water_dumux_nf = waterpotential_dumux[run, 0, timestep[i], 2:] - water_dumux_g = waterpotential_dumux[run, 1, timestep[i], 2:] + water_dumux_af = waterpotential_dumux[run, 1, timestep[i], 2:] + water_dumux_d = waterpotential_dumux[run, 2, timestep[i], 2:] water_steadyrate_nf = waterpotential_sr[run, 0, timestep[i], 2:] - water_steadyrate_g = waterpotential_sr[run, 1, timestep[i], 2:] + water_steadyrate_af = waterpotential_sr[run, 1, timestep[i], 2:] + water_steadyrate_d = waterpotential_sr[run, 2, timestep[i], 2:] solute_dumux_nf = solutes_dumux[run, 0, timestep[i], 2:] - solute_sr_nf = solutes_dumux_sr[run, 0, timestep[i], 2:] - solute_ss_nf = solutes_dumux_ss[run, 0, timestep[i], 2:] - solute_dumux_g = solutes_dumux[run, 1, timestep[i], 2:] - solute_sr_g = solutes_dumux_sr[run, 1, timestep[i], 2:] - solute_ss_g = solutes_dumux_ss[run, 1, timestep[i], 2:] - solute_ff_g = solutes_dumux_ff[run, 1, timestep[i], 2:] + solute_dumux_af = solutes_dumux[run, 1, timestep[i], 2:] + solute_dumux_d = solutes_dumux[run, 2, timestep[i], 2:] + + solutes_sr_simp_nf = solutes_sr_simp[run, 0, timestep[i], 2:] + solutes_sr_simp_af = solutes_sr_simp[run, 1, timestep[i], 2:] + solutes_sr_simp_d = solutes_sr_simp[run, 2, timestep[i], 2:] + + solutes_ss_af = solutes_ss[run, 1, timestep[i], 2:] + solutes_ss_d = solutes_ss[run, 2, timestep[i], 2:] + + solutes_sr_af = solutes_sr[run, 1, timestep[i], 2:] + solutes_sr_d = solutes_sr[run, 2, timestep[i], 2:] + + solutes_d_d = solutes_d[run, 2, timestep[i], 2:] + + solutes_u_nf = solutes_d[run, 0, timestep[i], 2:] + solutes_u_af = solutes_d[run, 1, timestep[i], 2:] + solutes_u_d = solutes_d[run, 2, timestep[i], 2:] + #left plot: no flux outer BC ax1[i,0].plot(CC, water_dumux_nf, "b", linestyle = linestyle_dumux, label = "water_dumux") ax1[i,0].plot(CC, water_steadyrate_nf, "b", linestyle = linestyle_steadyrate, label = "water_sr") ax2_0.plot(CC, solute_dumux_nf, "m", linestyle = linestyle_dumux, label = "solute_dumux") - ax2_0.plot(CC, solute_ss_nf, "m", linestyle = linestyle_steadystate, label = "solute_ss") - ax2_0.plot(CC, solute_sr_nf, "m", linestyle = linestyle_steadyrate, label = "solute_sr") + ax2_0.plot(CC, solutes_sr_simp_nf, "m", linestyle = linestyle_steadyrate, label = "solute_sr_simp") + ax2_0.plot(CC, solutes_u_nf, "m", linestyle = linestyle_dumux, label = "solute_u") + + #middle plot: water dirichlet, advective flow solutes + ax1[i,1].plot(CC, water_dumux_af, "b", linestyle = linestyle_dumux, label = "water_dumux") + ax1[i,1].plot(CC, water_steadyrate_af, "b", linestyle = linestyle_steadyrate, label = "water_sr") + ax2_1.plot(CC, solute_dumux_af, "m", linestyle = linestyle_dumux, label = "solute_dumux") + ax2_1.plot(CC, solutes_sr_simp_af, "m", linestyle = linestyle_steadyrate, label = "solute_sr_simp") + ax2_1.plot(CC, solutes_sr_af, "m", linestyle = linestyle_steadyrate, label = "solute_sr") + ax2_1.plot(CC, solutes_ss_af, "m", linestyle = linestyle_steadystate, label = "solute_ss") + ax2_1.plot(CC, solutes_u_af, "m", linestyle = linestyle_dumux, label = "solute_u") #right plot: Dirichlet (initial conditions) outer BC - ax1[i,1].plot(CC, water_dumux_g, "b", linestyle = linestyle_dumux, label = "water_dumux") - ax1[i,1].plot(CC, water_steadyrate_g, "b", linestyle = linestyle_steadyrate, label = "water_sr") - ax2_1.plot(CC, solute_dumux_g, "m", linestyle = linestyle_dumux, label = "solute_dumux") - ax2_1.plot(CC, solute_ss_g, "m", linestyle = linestyle_steadystate, label = "solute_ss") - ax2_1.plot(CC, solute_sr_g, "m", linestyle = linestyle_steadyrate, label = "solute_sr") - ax2_1.plot(CC, solute_ff_g, "m", linestyle = linestyle_special, label = "solute_ff") - #print(solute_ss_g) - #print(solute_sr_g) - #print(solutes_dumux[run, 1, timestep[i], :]) + ax1[i,2].plot(CC, water_dumux_d, "b", linestyle = linestyle_dumux, label = "water_dumux") + ax1[i,2].plot(CC, water_steadyrate_d, "b", linestyle = linestyle_steadyrate, label = "water_sr") + ax2_2.plot(CC, solute_dumux_d, "m", linestyle = linestyle_dumux, label = "solute_dumux") + ax2_2.plot(CC, solutes_sr_simp_d, "m", linestyle = linestyle_steadyrate, label = "solute_sr_simp") + ax2_2.plot(CC, solutes_sr_d, "m", linestyle = linestyle_steadyrate, label = "solute_sr") + ax2_2.plot(CC, solutes_ss_d, "m", linestyle = linestyle_steadystate, label = "solute_ss") + ax2_2.plot(CC, solutes_d_d, "m", linestyle = linestyle_special, label = "solute_d") + ax2_2.plot(CC, solutes_u_d, "m", linestyle = linestyle_dumux, label = "solute_u") + ax1[i,0].set_xlabel("distance root [cm]") ax1[i,0].set_ylabel("water") ax2_0.set_ylabel("nitrogen") @@ -479,58 +611,66 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu ax1[i,1].legend(loc="upper left") ax2_1.legend(loc="upper right") +ax1[i,2].set_xlabel("distance root [cm]") +ax1[i,2].set_ylabel("water") +ax2_2.set_ylabel("nitrogen") +ax1[i,2].legend(["watercontent cm3/cm3"], loc="upper left") +ax2_2.legend(["nitrogen concentration mol/cm3"], loc="upper right") + +ax1[i,2].legend(loc="upper left") +ax2_2.legend(loc="upper right") + #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) plt.show() -fig, ax1 = figure_style.subplots12(nrows=1, ncols=2) +fig, ax1 = figure_style.subplots12(nrows=1, ncols=3) # dumux(both) # left: sr no flux, ss for the no flux # right: ss, sr, farfield for sr waterflow +#load solute uptake suptake_dumux_nf = solutes_dumux[run, 0, 1:, 0] -suptake_dumux = solutes_dumux[run, 1, 1:, 0] - -solute_sr_nf = solutes_dumux_sr[run, 0, 1:, 0] -solute_ss_nf = solutes_dumux_ss[run, 0, 1:, 0] -solute_sr = solutes_dumux_sr[run, 1, 1:, 0] -solute_ss = solutes_dumux_ss[run, 1, 1:, 0] -solute_ff = solutes_dumux_ff[run, 1, 1:, 0] - -#TODO: test segment here, remove -solute_sr_nf_conc = solutes_dumux_sr[run, 0, 1:, 2] -solute_ss_nf_conc = solutes_dumux_ss[run, 0, 1:, 2] -solute_sr_conc = solutes_dumux_sr[run, 1, 1:, 2] -solute_ss_conc = solutes_dumux_ss[run, 1, 1:, 2] -solute_ff_conc = solutes_dumux_ff[run, 1, 1:, 2] - -Vmax = 4.0e-11 * 1 * (2*3.14*r_root) * (24*3600) #mol/(cm2 s) * cm * cm * (s/d) -> mol / d -Vmax_per_area = Vmax / (1 * (2*3.14*r_root)) #mol / d /cm2 = mol/(cm2d) -Km = 1.5e-7 #mol/cm3 - -#for i in range(len(solute_sr_nf_conc)): - #solute_sr_nf[i]=-Vmax_per_area*solute_sr_nf_conc[i]/(Km+solute_sr_nf_conc[i]) - #solute_ss_nf[i]=-Vmax_per_area*solute_ss_nf_conc[i]/(Km+solute_ss_nf_conc[i]) - #solute_sr[i]=-Vmax_per_area*solute_sr_conc[i]/(Km+solute_sr_conc[i]) - #solute_ss[i]=-Vmax_per_area*solute_ss_conc[i]/(Km+solute_ss_conc[i]) - #solute_ff[i]=-Vmax_per_area*solute_ff_conc[i]/(Km+solute_ff_conc[i]) - -#print(suptake_dumux) -#print(solute_ss) -#print(solutes_dumux_ss[run, 1, 1:, 2]) +suptake_dumux_af = solutes_dumux[run, 1, 1:, 0] +suptake_dumux_d = solutes_dumux[run, 2, 1:, 0] + +suptake_sr_simp_nf = solutes_sr_simp[run, 0, 1:, 0] +suptake_sr_simp_af = solutes_sr_simp[run, 1, 1:, 0] +suptake_sr_simp_d = solutes_sr_simp[run, 2, 1:, 0] + +suptake_ss_af = solutes_ss[run, 1, 1:, 0] +suptake_ss_d = solutes_ss[run, 2, 1:, 0] + +suptake_sr_af = solutes_sr[run, 1, 1:, 0] +suptake_sr_d = solutes_sr[run, 2, 1:, 0] + +suptake_d_d = solutes_d[run, 2, 1:, 0] + +suptake_u_nf = solutes_d[run, 0, 1:, 0] +suptake_u_af = solutes_d[run, 1, 1:, 0] +suptake_u_d = solutes_d[run, 2, 1:, 0] + + ax1[0].plot(suptake_dumux_nf, suptake_dumux_nf, "m", linestyle = linestyle_dumux, label = "dumux") -ax1[0].plot(suptake_dumux_nf, abs(solute_sr_nf), "m", linestyle = linestyle_steadyrate, label = "steady rate nf") -ax1[0].plot(suptake_dumux_nf, abs(solute_ss_nf), "m", linestyle = linestyle_steadystate, label = "steady state nf") -ax1[0].scatter(suptake_dumux_nf, abs(solute_ss_nf), marker = "*") -ax1[1].plot(suptake_dumux, suptake_dumux, "m", linestyle = linestyle_dumux, label = "dumux") -ax1[1].scatter(suptake_dumux, abs(suptake_dumux), marker = "*") -ax1[1].plot(suptake_dumux, abs(solute_sr), "m", linestyle = linestyle_steadyrate, label = "steady rate") -ax1[1].scatter(suptake_dumux, abs(solute_sr), marker = "*") -ax1[1].plot(suptake_dumux, abs(solute_ss), "m", linestyle = linestyle_steadystate, label = "steady state") -ax1[1].scatter(suptake_dumux, abs(solute_ss), marker = "*") -ax1[1].plot(suptake_dumux, abs(solute_ff), "m", linestyle = linestyle_special, label = "far field approximation") -ax1[1].scatter(suptake_dumux, abs(solute_ff), marker = "*") +ax1[0].scatter(suptake_dumux_nf, abs(suptake_dumux_nf), marker = "*") +ax1[0].plot(suptake_dumux_nf, abs(suptake_sr_simp_nf), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") +ax1[0].plot(suptake_dumux_nf, abs(suptake_u_nf), "g", linestyle = linestyle_steadyrate, label = "uniform") + +ax1[1].plot(suptake_dumux_af, suptake_dumux_af, "m", linestyle = linestyle_dumux, label = "dumux") +ax1[1].scatter(suptake_dumux_af, abs(suptake_dumux_af), marker = "*") +ax1[1].plot(suptake_dumux_af, abs(suptake_sr_simp_af), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") +ax1[1].plot(suptake_dumux_af, abs(suptake_sr_af), "p", linestyle = linestyle_steadyrate, label = "steady rate") +ax1[1].plot(suptake_dumux_af, abs(suptake_ss_af), "v", linestyle = linestyle_steadystate, label = "steady state") +ax1[1].plot(suptake_dumux_af, abs(suptake_u_af), "g", linestyle = linestyle_steadyrate, label = "uniform") + +ax1[2].plot(suptake_dumux_d, suptake_dumux_d, "m", linestyle = linestyle_dumux, label = "dumux") +ax1[2].scatter(suptake_dumux_d, abs(suptake_dumux_d), marker = "*") +ax1[2].plot(suptake_dumux_d, abs(suptake_sr_simp_d), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") +ax1[2].plot(suptake_dumux_d, abs(suptake_sr_d), "p", linestyle = linestyle_steadyrate, label = "steady rate") +ax1[2].plot(suptake_dumux_d, abs(suptake_ss_d), "v", linestyle = linestyle_steadystate, label = "steady state") +ax1[2].plot(suptake_dumux_d, abs(suptake_d_d), "c", linestyle = linestyle_special, label = "steady state") +ax1[2].plot(suptake_dumux_d, abs(suptake_u_d), "g", linestyle = linestyle_steadyrate, label = "uniform") ax1[0].set_xlabel("dumux solute uptake") ax1[0].set_ylabel("analytical approximation") @@ -540,6 +680,9 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu ax1[1].set_ylabel("analytical approximation") ax1[1].legend(["solute utake mol/cm2d"], loc="upper left") +ax1[2].set_xlabel("dumux solute uptake") +ax1[2].set_ylabel("analytical approximation") +ax1[2].legend(["solute utake mol/cm2d"], loc="upper left") #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) plt.show() diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 5077e6ad7..edccb82f1 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -51,6 +51,7 @@ def __init__(self, ms=None): self.alpha_0 = 0.3 # a constant that is used for numerically solving the perirhizal waterflow self.h_wilt = -16000 self.Ds0_ref = 1 # reference diffusion coefficient of solutes in water [cm2/d] + self.r0_ref = 1.0e-3 #reference lower bound on the radius[cm] self.water_filename = "lookup_perirhizal_waterflow_global" #name and location for the global lookup table def set_soil(self, sp): @@ -282,7 +283,7 @@ def solutesuptake_convdiff_(self, watercontent, c_bulk, Vmax, Km, Ds, waterflow, return rsc - def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km, Ds, sp, mode = "ff"): + def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prhiz, c_soil, c_outer, Vmax, Km, Ds, sp, mode = "ss"): """ steady rate assumption of solute uptake by roots TODO: insert citation @@ -293,7 +294,7 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ r_prhiz outer radius [cm] c_soil solute concentration of the cylinder [mol/cm3] c_outer solute concentration outside of the cylinder [mol/cm3], only used in the general steady rate case - Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] + Vmax maximum radial solute uptake rate, Michaelis Menten Kinetics [mol/(cmd)] Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] sp van Genuchten parameter set @@ -301,7 +302,8 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ "ss" for steady state solute uptake "sr" steady rate solute uptake with a no flux outer BC "ff" for steady rate uptake in combination with the far field approximation to give an outer Cauchy BC #TODO: cite paper for far field approximation - + "dirichlet" for Dirichlet outer BC + output: rsc solute concentration next to the root [mol/cm3] Uptake solute uptake of the root [mol/(cm d)] @@ -311,41 +313,49 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ n_segments = len(c_soil) - rsc = np.zeros(n_segments) #conentration next to the root + rsc = np.zeros(n_segments) #concentration next to the root Uptake = np.zeros(n_segments) #total solute uptake of the root quadratic_flow = np.zeros(n_segments) #quadratic flow into the parirhizal zone + noflux = np.zeros(n_segments) #concentration where diffusion and advection cancel out + + r_prhiz = r_prhiz.copy() # make sure that the original does not get scaled + r_root = r_root.copy() # make sure that the original does not get scaled #solve quadratic eqation for root uptake # TODO: Link to publication for i in range(n_segments): + #waterflow rho = r_prhiz[i] / r_root[i] Phi_0 = Phi_out[i] - 1/2 *(rootwateruptake[i] - waterinflow[i]) / (2*np.pi) * (rho**2) / (1 - rho**2) Phi_1 = waterinflow[i] / (2*np.pi) - (rootwateruptake[i] - waterinflow[i]) / (2*np.pi) * rho**2 / (1 - rho**2) Phi_2 = (rootwateruptake[i] - waterinflow[i]) / (2*np.pi) * (rho**2) / (1 - rho**2) - #are these next few lines even necessary? - Phi = lambda r : Phi_2 * (r/r_prhiz)**2/2 + Phi_1 * np.log(r/r_prhiz) + Phi_0 #warning: changing Phi_0 will also change this lambda function - waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) - watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) - radial_waterflow = lambda r : 2 * np.pi * (Phi_2 * (r/r_prhiz)**2 + Phi_1) - Ds_func = lambda r : Ds[i] * math.pow(watercontent_func(r),10/3) / (sp.theta_S**2) # Millington and Quirk Ds0 = self.Ds0_ref scaling = math.sqrt(Ds[i] / Ds0) # Ds_i > Ds0 means scaling >1, the region is modeled to be smaller than it actually is - scaling = Ds[i] / Ds0 # Ds_i > Ds0 means scaling >1, the region is modeled to be smaller than it actually is - Phi_2 = Phi_2 * ((scaling*r_prhiz[i])**2) + #scaling = Ds[i] / Ds0 # Ds_i > Ds0 means scaling >1, the region is modeled to be smaller than it actually is + Phi_2 = Phi_2 * (scaling**2) Phi_1 = Phi_1 - Phi_0 = Phi_0 + Phi_1 * np.log(scaling*r_prhiz[i]) - Phi = lambda r : Phi_2 * r**2/2 + Phi_1 * np.log(r) + Phi_0 #warning: changing Phi_0 will also change this lambda function - waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) #is the watercontent really necessary here? - watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) - radial_waterflow = lambda r : 2 * np.pi * (Phi_2 * r**2 + Phi_1) - Ds_func = lambda r : Ds[i] * math.pow(watercontent_func(r),10/3) / (sp.theta_S**2) # Millington and Quirk + Phi_0 = Phi_0 + Phi_1 * np.log(scaling) + #Phi = lambda r : Phi_2 * r**2/2 + Phi_1 * np.log(r) + Phi_0 + #waterpotential_func = lambda r : vg.fast_imfp[sp](Phi(r)) + #watercontent_func = lambda r : vg.water_content(waterpotential_func(r),sp) + #radial_waterflow = lambda r : 2 * np.pi * (Phi_2 * r**2 + Phi_1) + #Ds_func = lambda r : Ds[i] * math.pow(watercontent_func(r),10/3) / (sp.theta_S**2) # Millington and Quirk - #TODO: implement using the lookup table here - - r_eval = np.logspace(np.log10(r_root[i]), np.log10(r_prhiz[i]), num = 40) + r_eval = np.logspace(np.log10(r_root[i]), np.log10(r_prhiz[i]), num = 30) r_eval = [r / scaling for r in r_eval] + r_prhiz[i] = r_prhiz[i] / scaling + r_root[i] = r_root[i] / scaling - conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quatratic_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) + if self.lookup_table_sr_solutes: + for i in range(n): + conc_rel_c[i] = self.lookup_table_sr_solutes["conc_rel_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + conc_mean_c[i] = self.lookup_table_sr_solutes["conc_mean_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + Uptake_rel_c[i] = self.lookup_table_sr_solutes["Uptake_rel_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + Uptake_mean_c[i] = self.lookup_table_sr_solutes["Uptake_mean_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + quadratic_rel_c[i] = self.lookup_table_sr_solutes["quadratic_rel_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + quatratic_mean_c[i] = self.lookup_table_sr_solutes["quatratic_mean_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) + else: + conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quadratic_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) #default prefactors for the steady state case @@ -379,73 +389,82 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ #variables to eliminate: Uptake, quadratic, c_prhiz # variable for the final equations: rsc - A = np.zeros((3,3)) - b = np.zeros((3,2)) #right hand side of the linear equation, one for absolute values, one for the rsc value + A = np.zeros((4,4)) + b = np.zeros((4,2)) #right hand side of the linear equation, one for absolute values, one for the rsc value #linear equations for c_prhiz, Uptake, quadratic, rsc (3 linear, 1 quadratic) - #c11 * Uptake + c12 * quadratic + c13 * rsc = c_mean - A[0,0] = Uptake_mean_c[-1] - A[0,1] = quatratic_mean_c[-1] - b[0,1] = -conc_mean_c[-1] + #c11 * Uptake + c12 * quadratic + c13 * c0 = c_mean + A[0,0] = (Uptake_mean_c[-1]*(r_prhiz[i]**2)- Uptake_mean_c[0]*(r_root[i]**2))/(r_prhiz[i]**2-r_root[i]**2) + A[0,1] = (quadratic_mean_c[-1]*(r_prhiz[i]**2)- quadratic_mean_c[0]*(r_root[i]**2))/(r_prhiz[i]**2-r_root[i]**2) + A[0,2] = (conc_mean_c[-1]*(r_prhiz[i]**2)- conc_mean_c[0]*(r_root[i]**2))/(r_prhiz[i]**2-r_root[i]**2) b[0,0] = c_soil[i] - #c21 * Uptake + c22 * quadratic + c23 * rsc = c_prhiz + #c21 * Uptake + c22 * quadratic + c23 * c0 = c_prhiz A[1,0] = Uptake_rel_c[-1] A[1,1] = quadratic_rel_c[-1] - A[1,2] = -1 - b[1,1] = -conc_rel_c[-1] + A[1,2] = conc_rel_c[-1] + A[1,3] = -1 #pre_uptake * Uptake + pre_quadratic * quadratic + pre_c * c_prhiz = absolute A[2,0] = pre_Uptake A[2,1] = pre_quadratic - A[2,2] = pre_c + A[2,3] = pre_c b[2,0] = absolute + # c41 * Uptake + c42 * quadratic + c43 * c0 = rsc + A[3,0] = Uptake_rel_c[0] + A[3,1] = quadratic_rel_c[0] + A[3,2] = conc_rel_c[0] + b[3,1] = 1 + #m*rsc+n = [ss uptake, sr uptake, c(rprhiz), Uptake] n = la.solve(A,b[:,0]) m = la.solve(A,b[:,1]) #solve quadratic equation + #Uptake means radial uptake #Uptake = Vmax * rsc / (Km + rsc) - #Uptake = m[0]*rsc+n[0] - #(Km + rsc) * (m[0]*rsc+n[0]) = Vmax * rsc - # m[0] * rsc**2 + (Km[i]*m[0]+n[0]-Vmax[i]) * rsc + Km[i]*n[0] = 0 - + #Uptake = (m[0]+r_root**2 * m[1])*rsc+(n[0]+r_root**2 * n[1]) =: w * rsc + v + #(Km + rsc) * (w*rsc+v) = Vmax * rsc + # w * rsc**2 + (Km[i]*w+v-Vmax[i]) * rsc + Km[i]*v = 0 + w = (m[0] + (r_root[i]**2) * m[1]) #*(2*np.pi*r_root[i]) + v = (n[0] + (r_root[i]**2) * n[1]) #*(2*np.pi*r_root[i]) tol = 1e-9 #arbitrary for now, TODO think of a reasonable tolerance - if abs(m[0])0): rsc[i] = - p/2 - math.sqrt(p**2/4-q) else: rsc[i] = - p/2 + math.sqrt(p**2/4-q) - Uptake[i] = m[0]*rsc[i]+n[0] - quadratic_flow[i] = m[1]*rsc[i]+n[1] - - return rsc, Uptake, quadratic_flow + Uptake[i] = m[0] * rsc[i] + n[0] + quadratic_flow[i] = m[1] * rsc[i] + n[1] + noflux[i] = m[2] * rsc[i] + n[2] + + return rsc, Uptake, quadratic_flow, noflux - def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prhiz, r_eval, c_root, Ds0, Uptake, quadratic_flow, sp): + def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prhiz, r_eval, Ds0, Uptake, quadratic_flow, noflux, sp): """ given the water and solute uptake data this computes the discretisation of the steady rate solutions - Phi_out outer matrix flux potential [cm2/d] + Phi_out outer matrix flux potential [cm2/d] rootwateruptake radial root water uptake [cm2/d] waterinflow radial water inflow at r_prhiz [cm2/d] r_root root radius [cm] r_prhiz outer radius [cm] r_eval positions at which the solute concentration should be evaluated [cm] - c_root solute concentration next to the root [mol/cm3] Ds0 Diffusion constant in water [cm2/d] Uptake root of solutes [mol/(cm d)] quadratic_flow quadratic solute flow the perirhizal zone [mol/(cm3d)] + noflux solute concentration where diffusion and advection cancel each other out [mol/cm3] sp van Genuchten parameter set output: @@ -467,14 +486,12 @@ def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prh - - #solutes - #use the subfunction #TODO: alternative lookup table + #use the subfunction #scaling of the perirhizal zone for the diffusion coefficient scaling = math.sqrt(Ds0 / self.Ds0_ref) # Ds0 > Ds0_ref means scaling >1, the region is modeled to be smaller than it actually is - scaling = Ds0 / self.Ds0_ref # Ds0 > Ds0_ref means scaling >1, the region is modeled to be smaller than it actually is + #scaling = Ds0 / self.Ds0_ref # Ds0 > Ds0_ref means scaling >1, the region is modeled to be smaller than it actually is r_eval = [r / scaling for r in r_eval] Phi_2 = Phi_2 * (scaling**2) Phi_1 = Phi_1 @@ -482,7 +499,7 @@ def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prh #values necessary for plotting n = len(r_eval) - conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quatratic_mean_c = np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n) + conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quadratic_mean_c = np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n) if self.lookup_table_sr_solutes: for i in range(n): @@ -493,14 +510,12 @@ def watersolutes_disc(self, Phi_out, rootwateruptake, waterinflow, r_root, r_prh quadratic_rel_c[i] = self.lookup_table_sr_solutes["quadratic_rel_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) quatratic_mean_c[i] = self.lookup_table_sr_solutes["quatratic_mean_c"]((Phi_2, Phi_1, Phi_0, r_eval[i])) else: - conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quatratic_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) - - soluteconcentration = c_root * conc_rel_c + Uptake * Uptake_rel_c + quadratic_flow * quadratic_rel_c - soluteconcentration_mean = c_root * conc_mean_c + Uptake * Uptake_mean_c + quadratic_flow * quatratic_mean_c + conc_rel_c, conc_mean_c, Uptake_rel_c, Uptake_mean_c, quadratic_rel_c, quadratic_mean_c = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval, sp) + soluteconcentration = noflux * conc_rel_c + Uptake * Uptake_rel_c + quadratic_flow * quadratic_rel_c + soluteconcentration_mean = noflux * conc_mean_c + Uptake * Uptake_mean_c + quadratic_flow * quadratic_mean_c - - return watercontent, waterpotential, soluteconcentration + return watercontent, waterpotential, soluteconcentration, soluteconcentration_mean def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): """ @@ -524,8 +539,16 @@ def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): #reference diffusion coefficient to which r was scaled to [cm2/d] Ds0 = self.Ds0_ref - #r_root = r_eval[0] + #begin at a predefined n = len(r_eval) + r0 = self.r0_ref + insertion = False #was the radius inserted in the beginning? + if r_eval[0]==r0: + pass + else: + r_eval = np.insert(r_eval, 0, r0, axis=0) + insertion = True + n = n+1 conc_rel_c = np.ones(n) conc_mean_c = np.ones(n) @@ -543,9 +566,9 @@ def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): watercontent = lambda r : vg.water_content(vg.fast_imfp[sp](Phi(r)), sp) Ds = lambda r : Ds0 * math.pow(watercontent(r),10/3) / (sp.theta_S**2) - f_homogen = lambda c, r : ( radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference - f_steadystate = lambda c, r : (1 + radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference - f_quadratic = lambda c, r : (r**2 + radial_waterflow(r) * c) / (Ds(r) * r) #TODO: add reference + f_homogen = lambda c, r : ( radial_waterflow(r) * c) / (2 * np.pi * Ds(r) * r * watercontent(r)) #TODO: add reference + f_steadystate = lambda c, r : (1 + radial_waterflow(r) * c) / (2 * np.pi * Ds(r) * r * watercontent(r)) #TODO: add reference + f_quadratic = lambda c, r : (r**2 + radial_waterflow(r) * c) / (2 * np.pi * Ds(r) * r * watercontent(r)) #TODO: add reference #numerically solve the ODE c' = f(r_rel,c) starting at r_rel=1 conc_rel_c = odeint(f_homogen, y0 = 1, t = r_eval) @@ -559,12 +582,17 @@ def solute_linearequation_sr_(self, Phi_2, Phi_1, Phi_0, r_eval, sp): conc_mean_c[0] = conc_rel_c[0] inflow_mean_c[0] = inflow_rel_c[0] Uptake_mean_c[0] = Uptake_rel_c[0] - watercontent_times_r = np.array([watercontent(r)*r for r in r_eval]) + watercontent_times_r[0] = 0 + watercontent_times_r[1:] = np.array([watercontent(r_eval[i])*(r_eval[i]**2-r_eval[i-1]**2) for i in range(1,n)]) conc_mean_c[1:] = np.array([np.average(conc_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) inflow_mean_c[1:] = np.array([np.average(inflow_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) Uptake_mean_c[1:] = np.array([np.average(Uptake_rel_c[0:(j+1)], weights=watercontent_times_r[0:(j+1)]) for j in range(1,n)]) - return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c + #remove the inserted start at self.r0_ref + if insertion: + return conc_rel_c[1:], conc_mean_c[1:], inflow_rel_c[1:], inflow_mean_c[1:], Uptake_rel_c[1:], Uptake_mean_c[1:] + else: + return conc_rel_c, conc_mean_c, inflow_rel_c, inflow_mean_c, Uptake_rel_c, Uptake_mean_c def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r_prhiz, c_bulk, Vmax, Km, Ds, waterflow, sp, n_approx = 1): @@ -581,12 +609,12 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r Vmax maximum solute uptake rate, Michaelis Menten Kinetics [mol/(cm2d)] Km half saturation constant Michaelis Menten Kinetics [mol/cm3] Ds Diffusion constant in water [cm2/d] - waterflow steady state waterflow (entire root circumference) [cm2/d] + waterflow steady state waterflow [cm/d] sp van Genuchten parameter set n_approx how many points are used to approximate the perirhizal solute concentration? n_approx = 1 means only approximation at the perirhizal radius """ - assert len(Phi_root) == len(Phi_soil) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, c_bulk, Vmax, Km and waterflow must have the same length" + assert len(Phi_root) == len(Phi_soil) == len(r_root) == len(r_prhiz) == len(c_bulk) == len(Vmax) == len(Km) == len(waterflow), "Phi_root, Phi_soil, r_root, r_prhiz, c_bulk, Vmax, Km and waterflow must have the same length" n_segments = len(c_bulk) @@ -596,7 +624,7 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r x = [1] weights = [1] else: - [x,weights] = ts_roots(n_approx) # returns the nodes and weights for the chebyshev numterical integration + [x,weights] = ts_roots(n_approx) # returns the nodes and weights for the chebyshev numerical integration rho = [r_prhiz[i]/r_root[i] for i in range(n_segments)] @@ -645,11 +673,10 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r a2=(1-1/c_sol_mean2root[i])/(waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 - if p<0: + if (p<0) and (q>0): rsc[i]=-p/2-math.sqrt(pow(p/2,2)-q) else: - rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) - print("concentration sr nf f", rsc[i], -p/2-math.sqrt(pow(p/2,2)-q), -p/2+math.sqrt(pow(p/2,2)-q)) + rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) return rsc @@ -961,38 +988,63 @@ def create_integralAdvectionDiffusion_lookup(self, filename, sp): self.sp = sp return integral_overD_, base_mfp_ - def create_integralconcentration_lookup(self, filename, Ds_, sp): + def create_solute_sr_lookup(self, filename, sp): """ - Precomputes all integrals for the steady state solute flow + Precomputes for the steady rate solute flow - Phi upper matrix flux potential (bottom is set to 0) [cm2/d] - Ds_ array of all diffusion coefficients [cm2/d] + Phi_2, Phi_1, Phi_0 Phi = lambda r : Phi_2 * r**2/2 + Phi_1 * np.log(r) + Phi_0 + r_eval radii at which the conenctrations are avaluated sp van genuchten soil parameters, , call vg.create_mfp_lookup(sp) before """ #repeat steady state case - integral_overD_, base_mfp_ = self.create_integralDiffusion_lookup(filename, self.sp) n_Ds = len(Ds_) - Phin = 200 - Phi_ = np.logspace(np.log10(1.0e-6), np.log10(300), Phin) - outer_mfp_=Phi_ - inner_mfp_=Phi_ - R_sr_ = np.zeros((n_Ds,Phin,Phin)) - print("Creating a lookup table for the steady rate solute flow") - for i, Ds in enumerate(Ds_): - print("starting with diffusion coefficient number ", str(i+1), ":") - C_d = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) - for j, Phi_in in enumerate(inner_mfp_): - print(j) - for k, Phi_out in enumerate(outer_mfp_): - R_sr_[i,j,k] = self.integral_overconcentration_(C_d, Phi_in, Phi_out, 100.0, sp) - - np.savez(filename, integral_overD_=integral_overD_, base_mfp_=base_mfp_, Ds_ = Ds_, outer_mfp_ = outer_mfp_, inner_mfp_ = inner_mfp_, R_sr_ = R_sr_, soil = list(sp)) - self.lookup_table_sr_solutes = RegularGridInterpolator((Ds_, inner_mfp_, outer_mfp_) , R_sr_) - self.sp = sp + Phi_2_n = 100 + Phi_1_n = 100 + Phi_0_n = 100 + r_eval_n = 100 + r_eval_ = np.logspace(np.log(self.r0_ref), np.log(10)) + + conc_rel_c = np.zeros((Phi_2_n, Phi_1_n, Phi_0_n, r_eval_n)) + conc_mean_c = np.zeros((Phi_2_n, Phi_1_n, Phi_0_n, r_eval_n)) + Uptake_rel_c = np.zeros((Phi_2_n, Phi_1_n, Phi_0_n, r_eval_n)) + Uptake_mean_c = np.zeros((Phi_2_n, Phi_1_n, Phi_0_n, r_eval_n)) + quadratic_rel_c = np.zeros((Phi_2_n, Phi_1_n, Phi_0_n, r_eval_n)) + quatratic_mean_c = np.zeros((Phi_2_n, Phi_1_n, Phi_0_n, r_eval_n)) + + + Phi_2_ = -np.logspace(np.log(1.0e-5), np.log(1.0e-9), Phi_2_n) + print("Creating a big lookup table for the steady rate solute flow") + for i, Phi_2 in enumerate(Phi_2_): + print("Done with ", str(i+1), " out of ", str(Phi_2_n)) + Phi_1_ = np.logspace(np.log(1.0e-5), np.log(1.0e-1), Phi_1_n) + for j, Phi_1 in enumerate(Phi_1_): + Phi_0_ = np.logspace(np.log(1.0e-9), np.log(1.0e1), Phi_0_n) + for k, Phi_0 in enumerate(Phi_0_): + conc_rel_c[i,j,k,:], conc_mean_c[i,j,k,:], Uptake_rel_c[i,j,k,:], Uptake_mean_c[i,j,k,:], quadratic_rel_c[i,j,k,:], quatratic_mean_c[i,j,k,:] = self.solute_linearequation_sr_(Phi_2, Phi_1, Phi_0, r_eval_, sp) + + np.savez(filename, Phi_2_n=Phi_2_n, Phi_1_n=Phi_1_n, Phi_0_n = Phi_0_n, r_eval_n = r_eval_n, conc_rel_c = conc_rel_c, conc_mean_c = conc_mean_c, Uptake_rel_c = Uptake_rel_c, Uptake_mean_c = Uptake_mean_c, quatratic_rel_c = quatratic_rel_c, quatratic_mean_c = quatratic_mean_c, soil = list(sp)) + #like 3 GB? + self.lookup_table_sr_solutes = { + "conc_rel_c" : conc_rel_c, + "conc_mean_c" : conc_mean_c, + "Uptake_rel_c" : Uptake_rel_c, + "Uptake_mean_c" : Uptake_mean_c, + "quadratic_rel_c" : quadratic_rel_c, + "quatratic_mean_c" : quatratic_mean_c + } + self.lookup_table_sr_solutes["conc_rel_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , conc_rel_c) # method = "nearest" fill_value = None , bounds_error=False + self.lookup_table_sr_solutes["conc_mean_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , conc_mean_c) + self.lookup_table_sr_solutes["Uptake_rel_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , Uptake_rel_c) + self.lookup_table_sr_solutes["Uptake_mean_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , Uptake_mean_c) + self.lookup_table_sr_solutes["quadratic_rel_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , quadratic_rel_c) + self.lookup_table_sr_solutes["quatratic_mean_c"] = RegularGridInterpolator((Phi_2_, Phi_1_, Phi_0_, r_eval_) , quatratic_mean_c) + self.sp = vg.Parameters(soil) + vg.create_mfp_lookup(self.sp) # does this have to be repeated here? + def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): """ From 2806fe0cc6ab0a1c8bbc623642fd5df0f8be15b2 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Thu, 9 Jul 2026 22:09:28 +0200 Subject: [PATCH 37/41] plotting perirhizal solute implementation --- .../test_perirhizal.py | 272 ++++++++++++------ src/functional/Perirhizal.py | 17 +- 2 files changed, 191 insertions(+), 98 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index dc11dc31e..13920a7f7 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -38,11 +38,11 @@ do_computation = True #should the computation be run or take the data from a saved file # general parameters -max_time = 10 # d -n_times = 1000 # number of time intervals -r_prhiz = 0.5 # perirhizal radius[cm], computed for a RLD above 1cm/cm3 +max_time = 1 # d +n_times = 100 # number of time intervals +r_prhiz = 0.6 # perirhizal radius[cm], computed for a RLD above 1cm/cm3 r_root = 0.02 # root radius [cm] -NC = 40 # number of spatial discretisations +NC = 10 # number of spatial discretisations length = 1 #default length of the segment, will not change the outcpme as all variables are assumed constant in this direction [cm] #three scenarios will be computed: one without inflow, one with an advective flow (Dirichlet BC), another with a Dirichlet BC @@ -103,7 +103,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_d = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) solutes_u = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) - simtimes = np.linspace(0,max_time,n_times+1)[1:] + simtimes = np.linspace(0,max_time,n_times)#[1:] dt = max_time / n_times rho = r_prhiz / r_root @@ -151,7 +151,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu s.setParameter("Soil.BC.Top.SType", "3") # constantFluxCyl=3 (SType = Solute Type) s.setParameter("Soil.BC.Top.CValue", "0.0") else: - if s == af: + if s == s_af: s.setParameter("Soil.BC.Top.Type", "11") #dummy Dirichlet BC so dumux does not get mad s.setParameter("Soil.BC.Top.Value", str(initial_waterpotential)) s.setParameter("Soil.BC.Top.SType", "10") #advective flow @@ -173,7 +173,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu s.initializeProblem(maxDt = 0.01) s.ddt = 1.e-4 # days - cellVolumes = s_g.getCellSurfacesCyl() * length # cm3 + cellVolumes = s_d.getCellSurfacesCyl() * length # cm3 #initial solute concentration for the analytical approximations #simplified steady rate no flux (1d lookup table) @@ -184,17 +184,17 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #steady state solute flow solutes_ss = np.zeros((n_scenarios, n_times, NC+2)) - solutes_ss[0,0,2:]=np.ones(NC) * initial_soluteconcentration solutes_ss[1,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_ss[2,0,2:]=np.ones(NC) * initial_soluteconcentration #steady rate no flux outer BC solute flow in the general water flow solutes_sr = np.zeros((n_scenarios, n_times, NC+2)) - solutes_sr[0,0,2:]=np.ones(NC) * initial_soluteconcentration solutes_sr[1,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_sr[2,0,2:]=np.ones(NC) * initial_soluteconcentration #Dirichlet BC solutes_d = np.zeros((n_scenarios, n_times, NC+2)) - solutes_d[0,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_d[2,0,2:]=np.ones(NC) * initial_soluteconcentration #uniform concentration (= no analytical approximation) solutes_u = np.zeros((n_scenarios, n_times, NC+2)) @@ -245,11 +245,11 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu for r in range(1,len(simtimes)): - dt = time[r] + dt = simtimes[r] if r>0: - dt = time[r] - simtimes[r-1] + dt = simtimes[r] - simtimes[r-1] - print('time',time) + print('time',simtimes[r]) print('no flux BC') print("*****", "#", r, "external time step", dt, " d, simulation time", s_nf.simTime, "d, internal time step", s_nf.ddt, "d") print('advective flow') @@ -260,18 +260,18 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #one timestep s_nf.solve(dt, saveInnerFluxes_ = True) s_af.solve(dt, saveInnerFluxes_ = True) - s_d.solve(dt, saveInnerFluxes_ = True) + s_d.solve(dt, saveInnerFluxes_ = True) #TODO: this has numerical problems #watercontent and solute content, discretised watercontent_nf = s_nf.getWaterContent() # cm3 waterpotential_nf = np.array([vg.pressure_head(watercontent_nf[i],peri.sp) for i in range(NC)]) # cm watercontent_af = s_af.getWaterContent() # cm3 - waterpotential_af = np.array([vg.pressure_head(watercontent_g[i],peri.sp) for i in range(NC)]) # cm + waterpotential_af = np.array([vg.pressure_head(watercontent_af[i],peri.sp) for i in range(NC)]) # cm watercontent_d = s_d.getWaterContent() # cm3 - waterpotential_d = np.array([vg.pressure_head(watercontent_g[i],peri.sp) for i in range(NC)]) # cm - solutes_nf = s_nf.getSolution(1) / molarMassSolute # mol/cm3 - solutes_af = s_af.getSolution(1) / molarMassSolute # mol/cm3 - solutes_d = s_d.getSolution(1) / molarMassSolute # mol/cm3 + waterpotential_d = np.array([vg.pressure_head(watercontent_d[i],peri.sp) for i in range(NC)]) # cm + solutecontents_nf = s_nf.getSolution(1) / molarMassSolute # mol/cm3 + solutecontents_af = s_af.getSolution(1) / molarMassSolute # mol/cm3 + solutecontents_d = s_d.getSolution(1) / molarMassSolute # mol/cm3 #inflow and outflow rootuptake_w_nf = s_nf.getInnerFlow(0, length) /(length*(2*np.pi*r_root)) # cm /d @@ -312,7 +312,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu waterpotential_dumux[0,r,2:]=waterpotential_nf solutes_dumux[0,r,0]=rootuptake_s_nf solutes_dumux[0,r,1]=inflow_s_nf - solutes_dumux[0,r,2:]=solutes_nf + solutes_dumux[0,r,2:]=solutecontents_nf #advective flow scenario watercontent_dumux[1,r,0]=rootuptake_w_af watercontent_dumux[1,r,1]=inflow_w_af @@ -322,35 +322,39 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu waterpotential_dumux[1,r,2:]=waterpotential_af solutes_dumux[1,r,0]=rootuptake_s_af solutes_dumux[1,r,1]=inflow_s_af - solutes_dumux[1,r,2:]=solutes_af + solutes_dumux[1,r,2:]=solutecontents_af #Dirichlet BC - watercontent_dumux[1,r,0]=rootuptake_w_d - watercontent_dumux[1,r,1]=inflow_w_d - watercontent_dumux[1,r,2:]=watercontent_d - waterpotential_dumux[1,r,0]=rootuptake_w_d - waterpotential_dumux[1,r,1]=inflow_w_d - waterpotential_dumux[1,r,2:]=waterpotential_d - solutes_dumux[1,r,0]=rootuptake_s_d - solutes_dumux[1,r,1]=inflow_s_d - solutes_dumux[1,r,2:]=solutes_d + watercontent_dumux[2,r,0]=rootuptake_w_d + watercontent_dumux[2,r,1]=inflow_w_d + watercontent_dumux[2,r,2:]=watercontent_d + waterpotential_dumux[2,r,0]=rootuptake_w_d + waterpotential_dumux[2,r,1]=inflow_w_d + waterpotential_dumux[2,r,2:]=waterpotential_d + solutes_dumux[2,r,0]=rootuptake_s_d + solutes_dumux[2,r,1]=inflow_s_d + solutes_dumux[2,r,2:]=solutecontents_d - #determine means + #determine means + #for the explicit Euler method of the timesteps + f_root = (2*np.pi*r_root)/(np.pi*(r_prhiz**2-r_root**2)) + f_prhiz = Ds* pow(0.25,13/3)/(0.4**2)*(2*np.pi*r_prhiz)/(np.pi*(r_prhiz**2-r_root**2)) #only have diffusion on the outside for now, assume water content of 0.25, saturated watercontent of 0.4 + IC = initial_soluteconcentration #no flux outer BC mean_watercontent_nf = np.average(watercontent_nf, weights=volumes) mean_waterpotential_nf = vg.pressure_head(mean_watercontent_nf, peri.sp) - mean_soluteconcent_nf = np.average(solutes_nf, weights=np.multiply(mean_watercontent_nf, volumes)) - mean_soluteconcent_sr_simp_nf = np.average(solutes_sr_simp[0,r,2:], weights=np.multiply(mean_watercontent_nf, volumes)) + mean_soluteconcent_nf = np.average(solutecontents_nf, weights=np.multiply(mean_watercontent_nf, volumes)) + mean_soluteconcent_sr_simp_nf = np.average(solutes_sr_simp[0,r-1,2:], weights=np.multiply(watercontent_nf, volumes))+(f_root*solutes_sr_simp[0,r-1,0])*dt #advective flow mean_watercontent_af = np.average(watercontent_af, weights=volumes) mean_waterpotential_af = vg.pressure_head(mean_watercontent_af, peri.sp) - mean_soluteconcent_af = np.average(solutes_af, weights=np.multiply(mean_watercontent_af, volumes)) - mean_soluteconcent_sr_simp_af = np.average(solutes_sr_simp[1,r,2:], weights=np.multiply(mean_watercontent_af, volumes)) + mean_soluteconcent_af = np.average(solutecontents_af, weights=np.multiply(mean_watercontent_af, volumes)) + mean_soluteconcent_sr_simp_af = np.average(solutes_sr_simp[1,r-1,2:], weights=np.multiply(watercontent_af, volumes))+(f_root*solutes_sr_simp[1,r-1,0])*dt #Dirichlet BC mean_watercontent_d = np.average(watercontent_d, weights=volumes) mean_waterpotential_d = vg.pressure_head(mean_watercontent_d, peri.sp) - mean_soluteconcent_d = np.average(solutes_d, weights=np.multiply(mean_watercontent_d, volumes)) - mean_soluteconcent_sr_simp_d = np.average(solutes_sr_simp[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes)) - + mean_soluteconcent_d = np.average(solutecontents_d, weights=np.multiply(mean_watercontent_d, volumes)) + mean_soluteconcent_sr_simp_d = np.average(solutes_sr_simp[2,r-1,2:], weights=np.multiply(watercontent_d, volumes))+(f_root*solutes_sr_simp[2,r-1,0]+f_prhiz*(IC-solutes_sr_simp[2,r-1,-1]))*dt + print("mean", mean_soluteconcent_sr_simp_d, mean_soluteconcent_sr_simp_nf, f_root, dt, (f_root*solutes_sr_simp[0,r-1,0])*dt) #determine coefficients for the analytical approximation #equation [4] in Schroeder2008 doi:10.2136/vzj2007.0114 #Phi(r)=Phi_outer + (q_root*r_root-q_out*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-ln(r/r_prhiz))+q_out*r_prhiz*ln(r/r_prhiz) @@ -397,9 +401,9 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #compute the analytical approximations for the solute uptake #case of dumux no flux outer BC - result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_nf], [Vmax_per_area], [Km], Ds, [-waterdemand], peri.sp, n_approx = 10) - result_solutes_sr_af = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_af], [Phi_soil_af], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_af], [Vmax_per_area], [Km], Ds, [-waterdemand], peri.sp, n_approx = 10) - result_solutes_sr_d = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_d], [Phi_soil_d], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_d], [Vmax_per_area], [Km], Ds, [-waterdemand], peri.sp, n_approx = 10) + result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 10) + result_solutes_sr_af = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_af], [Phi_soil_af], [r_root], [r_prhiz], [mean_soluteconcent_af], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 10) + result_solutes_sr_d = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_d], [Phi_soil_d], [r_root], [r_prhiz], [mean_soluteconcent_d], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 10) #safe the results #(here there is no computed inflow, so index 1 doesn't do anything) @@ -409,6 +413,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_sr_simp[1,r,0]=-Vmax_per_area * result_solutes_sr_af / (Km + result_solutes_sr_af) result_solutes_sr_d = result_solutes_sr_d[0] solutes_sr_simp[2,r,0]=-Vmax_per_area * result_solutes_sr_d / (Km + result_solutes_sr_d) + print("Uptake", solutes_sr_simp[2,r,0], result_solutes_sr_d) F0_nf = peri.integral_AdvectionDiffusion_(Phi_root_nf,peri.sp) F0_af = peri.integral_AdvectionDiffusion_(Phi_root_af,peri.sp) @@ -420,62 +425,64 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu r_current = CC[j] F_nf = peri.integral_AdvectionDiffusion_(Phi_nf(r_current),peri.sp)-F0_nf F_af = peri.integral_AdvectionDiffusion_(Phi_af(r_current),peri.sp)-F0_af - F_g = peri.integral_AdvectionDiffusion_(Phi_g(r_current),peri.sp)-F0_g + F_d = peri.integral_AdvectionDiffusion_(Phi_d(r_current),peri.sp)-F0_d F_tilde_nf=math.exp(D_tilde*F_nf) F_tilde_af=math.exp(D_tilde*F_af) F_tilde_d=math.exp(D_tilde*F_d) - solutes_sr_simp[0,r,2+j] = result_solutes_sr_nf * F_tilde_nf - (1-F_tilde_nf) * solutes_sr_simp[0,r,0] / waterdemand - solutes_sr_simp[1,r,2+j] = result_solutes_sr_af * F_tilde_af - (1-F_tilde_af) * solutes_sr_simp[1,r,0] / waterdemand - solutes_sr_simp[2,r,2+j] = result_solutes_sr_d * F_tilde_d - (1-F_tilde_d) * solutes_sr_simp[2,r,0] / waterdemand + solutes_sr_simp[0,r,2+j] = result_solutes_sr_nf / F_tilde_nf + (1-1/F_tilde_nf) * solutes_sr_simp[0,r,0] / (waterdemand) + solutes_sr_simp[1,r,2+j] = result_solutes_sr_af / F_tilde_af + (1-1/F_tilde_af) * solutes_sr_simp[1,r,0] / (waterdemand) + solutes_sr_simp[2,r,2+j] = result_solutes_sr_d / F_tilde_d + (1-1/F_tilde_d) * solutes_sr_simp[2,r,0] / (waterdemand) # case of general steady rate water uptake # safe the means, they are computed via the explicit Euler timestepping scheme - mean_soluteconcent_ss_af = np.average(solutes_ss[1,r,2:], weights=np.multiply(mean_watercontent_af, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_ss[1,r,0]*dt #TODO: add inflow from outside - mean_soluteconcent_ss_d = np.average(solutes_ss[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_ss[2,r,0]*dt - mean_soluteconcent_sr_af = np.average(solutes_sr[1,r,2:], weights=np.multiply(mean_watercontent_af, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_sr[1,r,0]*dt - mean_soluteconcent_sr_d = np.average(solutes_sr[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_sr[2,r,0]*dt - mean_soluteconcent_d_d = np.average(solutes_d[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_d[2,r,0]*dt - mean_soluteconcent_u_nf = np.average(solutes_u[0,r,2:], weights=np.multiply(mean_watercontent_nf, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_u[0,r,0]*dt - mean_soluteconcent_u_af = np.average(solutes_u[1,r,2:], weights=np.multiply(mean_watercontent_af, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_u[1,r,0]*dt - mean_soluteconcent_u_d = np.average(solutes_u[2,r,2:], weights=np.multiply(mean_watercontent_d, volumes))+(2*r_root)/(r_prhiz**2-r_root**2)*solutes_u[2,r,0]*dt + # TODO: technically the concentrations of the last timestep and the current watercontents are mixed, shoul dnot be an issue for small timesteps + + mean_soluteconcent_ss_af = np.average(solutes_ss[1,r-1,2:], weights=np.multiply(mean_watercontent_af, volumes))+f_root*solutes_ss[1,r-1,0]*dt #TODO: add inflow from outside + mean_soluteconcent_ss_d = np.average(solutes_ss[2,r-1,2:], weights=np.multiply(mean_watercontent_d, volumes))+(f_root*solutes_ss[2,r-1,0]+f_prhiz*(IC-solutes_ss[2,r-1,-1]))*dt + mean_soluteconcent_sr_af = np.average(solutes_sr[1,r-1,2:], weights=np.multiply(mean_watercontent_af, volumes))+f_root*solutes_sr[1,r-1,0]*dt + mean_soluteconcent_sr_d = np.average(solutes_sr[2,r-1,2:], weights=np.multiply(mean_watercontent_d, volumes))+f_root*solutes_sr[2,r-1,0]*dt + mean_soluteconcent_d_d = np.average(solutes_d[2,r-1,2:], weights=np.multiply(mean_watercontent_d, volumes))+(f_root*solutes_d[2,r-1,0]+f_prhiz*(IC-solutes_d[1,r-1,-1]))*dt + mean_soluteconcent_u_nf = np.average(solutes_u[0,r-1,2:], weights=np.multiply(mean_watercontent_nf, volumes))+f_root*solutes_u[0,r-1,0]*dt + mean_soluteconcent_u_af = np.average(solutes_u[1,r-1,2:], weights=np.multiply(mean_watercontent_af, volumes))+f_root*solutes_u[1,r-1,0]*dt + mean_soluteconcent_u_d = np.average(solutes_u[2,r-1,2:], weights=np.multiply(mean_watercontent_d, volumes))+(f_root*solutes_u[2,r-1,0]+f_prhiz*(IC-solutes_u[2,r-1,-1]))*dt #for the steady state take again the outer concentration #general steady state - rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_af], [rootuptake_w_af*2*np.pi*r_root], [inflow_w_af*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_ss_af], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") - _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) - solutes_dumux_ss[1,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_dumux_ss[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) - solutes_dumux_ss[1,r,2:] = soluteconcentration[:] - rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_ss_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") - _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) - solutes_dumux_ss[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_dumux_ss[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) - solutes_dumux_ss[2,r,2:] = soluteconcentration[:] + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_af], [rootuptake_w_af*2*np.pi*r_root], [inflow_w_af*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_af], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_af, 2*np.pi * rootuptake_w_af*r_root, 2*np.pi * inflow_w_af*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_ss[1,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + solutes_ss[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_ss[1,r,2:] = soluteconcentration[:] + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_d, 2*np.pi * rootuptake_w_d*r_root, 2*np.pi * inflow_w_d*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_ss[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + solutes_ss[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_ss[2,r,2:] = soluteconcentration[:] #general steady rate no flux outer BC - rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_af], [rootuptake_w_af*2*np.pi*r_root], [inflow_w_af*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_sr_af], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") - _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) - solutes_dumux_sr[1,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_dumux_sr[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) - solutes_dumux_sr[1,r,2:] = soluteconcentration[:] - rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_sr_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") - _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) - solutes_dumux_sr[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_dumux_sr[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) - solutes_dumux_sr[2,r,2:] = soluteconcentration[:] + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_af], [rootuptake_w_af*2*np.pi*r_root], [inflow_w_af*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_af], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_af, 2*np.pi * rootuptake_w_af*r_root, 2*np.pi * inflow_w_af*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_sr[1,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + solutes_sr[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_sr[1,r,2:] = soluteconcentration[:] + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_d, 2*np.pi * rootuptake_w_d*r_root, 2*np.pi * inflow_w_d*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_sr[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + solutes_sr[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_sr[2,r,2:] = soluteconcentration[:] #Dirichlet BC - rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_d_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "dirichlet") - _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_g, 2*np.pi * rootuptake_w_g*r_root, 2*np.pi * inflow_w_g*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) - solutes_dumux_d[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_dumux_d[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) - solutes_dumux_d[2,r,2:] = soluteconcentration[:] + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "dirichlet") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_d, 2*np.pi * rootuptake_w_d*r_root, 2*np.pi * inflow_w_d*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + solutes_d[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + solutes_d[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_d[2,r,2:] = soluteconcentration[:] #uniform concentration - solutes_dumux_u[0,r,0] = -Vmax_per_area*mean_soluteconcent_u_nf/(Km + mean_soluteconcent_u_nf) - solutes_dumux_u[1,r,0] = -Vmax_per_area*mean_soluteconcent_u_af/(Km + mean_soluteconcent_u_af) - solutes_dumux_u[2,r,0] = -Vmax_per_area*mean_soluteconcent_u_d/(Km + mean_soluteconcent_u_d) - solutes_dumux_u[0,r,2:] = np.ones(NC) * mean_soluteconcent_u_nf - solutes_dumux_u[1,r,2:] = np.ones(NC) * mean_soluteconcent_u_af - solutes_dumux_u[2,r,2:] = np.ones(NC) * mean_soluteconcent_u_d + solutes_u[0,r,0] = -Vmax_per_area*mean_soluteconcent_u_nf/(Km + mean_soluteconcent_u_nf) + solutes_u[1,r,0] = -Vmax_per_area*mean_soluteconcent_u_af/(Km + mean_soluteconcent_u_af) + solutes_u[2,r,0] = -Vmax_per_area*mean_soluteconcent_u_d/(Km + mean_soluteconcent_u_d) + solutes_u[0,r,2:] = np.ones(NC) * mean_soluteconcent_u_nf + solutes_u[1,r,2:] = np.ones(NC) * mean_soluteconcent_u_af + solutes_u[2,r,2:] = np.ones(NC) * mean_soluteconcent_u_d return watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_sr_simp, solutes_ss, solutes_sr, solutes_d, solutes_u @@ -537,7 +544,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu for i in range(5): ax2_0 = ax1[i,0].twinx() ax2_1 = ax1[i,1].twinx() - ax2_2 = ax1[i,1].twinx() + ax2_2 = ax1[i,2].twinx() #load data water_dumux_nf = waterpotential_dumux[run, 0, timestep[i], 2:] @@ -562,9 +569,9 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_d_d = solutes_d[run, 2, timestep[i], 2:] - solutes_u_nf = solutes_d[run, 0, timestep[i], 2:] - solutes_u_af = solutes_d[run, 1, timestep[i], 2:] - solutes_u_d = solutes_d[run, 2, timestep[i], 2:] + solutes_u_nf = solutes_u[run, 0, timestep[i], 2:] + solutes_u_af = solutes_u[run, 1, timestep[i], 2:] + solutes_u_d = solutes_u[run, 2, timestep[i], 2:] #left plot: no flux outer BC @@ -647,9 +654,9 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu suptake_d_d = solutes_d[run, 2, 1:, 0] -suptake_u_nf = solutes_d[run, 0, 1:, 0] -suptake_u_af = solutes_d[run, 1, 1:, 0] -suptake_u_d = solutes_d[run, 2, 1:, 0] +suptake_u_nf = solutes_u[run, 0, 1:, 0] +suptake_u_af = solutes_u[run, 1, 1:, 0] +suptake_u_d = solutes_u[run, 2, 1:, 0] ax1[0].plot(suptake_dumux_nf, suptake_dumux_nf, "m", linestyle = linestyle_dumux, label = "dumux") @@ -686,3 +693,86 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) plt.show() + + +fig, ax1 = figure_style.subplots12(nrows=1, ncols=3) +# dumux(both) +# left: sr no flux, ss for the no flux +# right: ss, sr, farfield for sr waterflow + +simtimes = np.linspace(0,max_time,n_times) + +#load solute uptake +suptake_dumux_nf = solutes_dumux[run, 0, 1:, 0] +suptake_dumux_af = solutes_dumux[run, 1, 1:, 0] +suptake_dumux_d = solutes_dumux[run, 2, 1:, 0] + +suptake_sr_simp_nf = solutes_sr_simp[run, 0, 1:, 0] +suptake_sr_simp_af = solutes_sr_simp[run, 1, 1:, 0] +suptake_sr_simp_d = solutes_sr_simp[run, 2, 1:, 0] + +suptake_ss_af = solutes_ss[run, 1, 1:, 0] +suptake_ss_d = solutes_ss[run, 2, 1:, 0] + +suptake_sr_af = solutes_sr[run, 1, 1:, 0] +suptake_sr_d = solutes_sr[run, 2, 1:, 0] + +suptake_d_d = solutes_d[run, 2, 1:, 0] + +suptake_u_nf = solutes_u[run, 0, 1:, 0] +suptake_u_af = solutes_u[run, 1, 1:, 0] +suptake_u_d = solutes_u[run, 2, 1:, 0] + +suptake_dumux_nf = np.array([ sum(suptake_dumux_nf[:i]) for i in range(n_times)]) +suptake_dumux_af = np.array([ sum(suptake_dumux_af[:i]) for i in range(n_times)]) +suptake_dumux_d = np.array([ sum(suptake_dumux_d[:i]) for i in range(n_times)]) + +suptake_sr_simp_nf = np.array([ sum(suptake_sr_simp_nf[:i]) for i in range(n_times)]) +suptake_sr_simp_af = np.array([ sum(suptake_sr_simp_af[:i]) for i in range(n_times)]) +suptake_sr_simp_d = np.array([ sum(suptake_sr_simp_d[:i]) for i in range(n_times)]) + +suptake_ss_af = np.array([ sum(suptake_ss_af[:i]) for i in range(n_times)]) +suptake_ss_d = np.array([ sum(suptake_ss_d[:i]) for i in range(n_times)]) + +suptake_sr_af = np.array([ sum(suptake_sr_af[:i]) for i in range(n_times)]) +suptake_sr_d = np.array([ sum(suptake_sr_d[:i]) for i in range(n_times)]) + +suptake_d_d = np.array([ sum(suptake_d_d[:i]) for i in range(n_times)]) + +suptake_u_nf = np.array([ sum(suptake_u_nf[:i]) for i in range(n_times)]) +suptake_u_af = np.array([ sum(suptake_u_af[:i]) for i in range(n_times)]) +suptake_u_d = np.array([ sum(suptake_u_d[:i]) for i in range(n_times)]) + + + +ax1[0].plot(simtimes, abs(suptake_dumux_nf), "m", linestyle = linestyle_dumux, label = "dumux") +ax1[0].plot(simtimes, abs(suptake_sr_simp_nf), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") +ax1[0].plot(simtimes, abs(suptake_u_nf), "g", linestyle = linestyle_steadyrate, label = "uniform") + +ax1[1].plot(simtimes, abs(suptake_dumux_af), "m", linestyle = linestyle_dumux, label = "dumux") +ax1[1].plot(simtimes, abs(suptake_sr_simp_af), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") +ax1[1].plot(simtimes, abs(suptake_sr_af), "p", linestyle = linestyle_steadyrate, label = "steady rate") +ax1[1].plot(simtimes, abs(suptake_ss_af), "v", linestyle = linestyle_steadystate, label = "steady state") +ax1[1].plot(simtimes, abs(suptake_u_af), "g", linestyle = linestyle_steadyrate, label = "uniform") + +ax1[2].plot(simtimes, abs(suptake_dumux_d), "m", linestyle = linestyle_dumux, label = "dumux") +ax1[2].plot(simtimes, abs(suptake_sr_simp_d), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") +ax1[2].plot(simtimes, abs(suptake_sr_d), "p", linestyle = linestyle_steadyrate, label = "steady rate") +ax1[2].plot(simtimes, abs(suptake_ss_d), "v", linestyle = linestyle_steadystate, label = "steady state") +ax1[2].plot(simtimes, abs(suptake_d_d), "c", linestyle = linestyle_special, label = "steady state") +ax1[2].plot(simtimes, abs(suptake_u_d), "g", linestyle = linestyle_steadyrate, label = "uniform") + +ax1[0].set_xlabel("dumux solute uptake") +ax1[0].set_ylabel("analytical approximation") +ax1[0].legend(["solute utake mol/cm2d"], loc="upper left") + +ax1[1].set_xlabel("dumux solute uptake") +ax1[1].set_ylabel("analytical approximation") +ax1[1].legend(["solute utake mol/cm2d"], loc="upper left") + +ax1[2].set_xlabel("dumux solute uptake") +ax1[2].set_ylabel("analytical approximation") +ax1[2].legend(["solute utake mol/cm2d"], loc="upper left") + +#np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +plt.show() diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index edccb82f1..44234d376 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -441,10 +441,12 @@ def soil_root_solutes_sr(self, Phi_out, rootwateruptake, waterinflow, r_root, r_ C_qe = Km[i]*v p = B_qe/A_qe q = C_qe/A_qe - if (p<0) and (q>0): - rsc[i] = - p/2 - math.sqrt(p**2/4-q) + r1=- p/2 - math.sqrt(p**2/4-q) + r2=- p/2 + math.sqrt(p**2/4-q) + if r1<0: + rsc[i]=r2 else: - rsc[i] = - p/2 + math.sqrt(p**2/4-q) + rsc[i]=r1 Uptake[i] = m[0] * rsc[i] + n[0] quadratic_flow[i] = m[1] * rsc[i] + n[1] noflux[i] = m[2] * rsc[i] + n[2] @@ -673,11 +675,12 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r a2=(1-1/c_sol_mean2root[i])/(waterflow[i]) p=Km[i]-a2*Vmax[i]-a1 q=-Km[i]*a1 - if (p<0) and (q>0): - rsc[i]=-p/2-math.sqrt(pow(p/2,2)-q) + r1=-p/2-math.sqrt(pow(p/2,2)-q) + r2=-p/2+math.sqrt(pow(p/2,2)-q) + if r1<0: + rsc[i]=r2 else: - rsc[i]=-p/2+math.sqrt(pow(p/2,2)-q) - + rsc[i]=r1 return rsc @staticmethod From 42ab4ebd230d3aaa4b3f9aaa81a4d245b24d81a0 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Sun, 12 Jul 2026 19:20:03 +0200 Subject: [PATCH 38/41] updated means --- .../test_perirhizal.py | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index 13920a7f7..474e8caa9 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -178,29 +178,29 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #initial solute concentration for the analytical approximations #simplified steady rate no flux (1d lookup table) solutes_sr_simp = np.zeros((n_scenarios, n_times, NC+2)) - solutes_sr_simp[0,0,2:]=np.ones(NC) * initial_soluteconcentration - solutes_sr_simp[1,0,2:]=np.ones(NC) * initial_soluteconcentration - solutes_sr_simp[2,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_sr_simp[0,0,1]= initial_soluteconcentration + solutes_sr_simp[1,0,1]= initial_soluteconcentration + solutes_sr_simp[2,0,1]= initial_soluteconcentration #steady state solute flow solutes_ss = np.zeros((n_scenarios, n_times, NC+2)) - solutes_ss[1,0,2:]=np.ones(NC) * initial_soluteconcentration - solutes_ss[2,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_ss[1,0,1]= initial_soluteconcentration + solutes_ss[2,0,1]= initial_soluteconcentration #steady rate no flux outer BC solute flow in the general water flow solutes_sr = np.zeros((n_scenarios, n_times, NC+2)) - solutes_sr[1,0,2:]=np.ones(NC) * initial_soluteconcentration - solutes_sr[2,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_sr[1,0,1]= initial_soluteconcentration + solutes_sr[2,0,1]= initial_soluteconcentration #Dirichlet BC solutes_d = np.zeros((n_scenarios, n_times, NC+2)) - solutes_d[2,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_d[2,0,1]= initial_soluteconcentration #uniform concentration (= no analytical approximation) solutes_u = np.zeros((n_scenarios, n_times, NC+2)) - solutes_u[0,0,2:]=np.ones(NC) * initial_soluteconcentration - solutes_u[1,0,2:]=np.ones(NC) * initial_soluteconcentration - solutes_u[2,0,2:]=np.ones(NC) * initial_soluteconcentration + solutes_u[0,0,1]= initial_soluteconcentration + solutes_u[1,0,1]= initial_soluteconcentration + solutes_u[2,0,1]= initial_soluteconcentration #variables for the no flux case @@ -343,17 +343,20 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu mean_watercontent_nf = np.average(watercontent_nf, weights=volumes) mean_waterpotential_nf = vg.pressure_head(mean_watercontent_nf, peri.sp) mean_soluteconcent_nf = np.average(solutecontents_nf, weights=np.multiply(mean_watercontent_nf, volumes)) - mean_soluteconcent_sr_simp_nf = np.average(solutes_sr_simp[0,r-1,2:], weights=np.multiply(watercontent_nf, volumes))+(f_root*solutes_sr_simp[0,r-1,0])*dt + mean_soluteconcent_sr_simp_nf = solutes_sr_simp[0,r-1,1]+(f_root*solutes_sr_simp[0,r-1,0])*dt + solutes_sr_simp[0,r,1] = mean_soluteconcent_sr_simp_nf #advective flow mean_watercontent_af = np.average(watercontent_af, weights=volumes) mean_waterpotential_af = vg.pressure_head(mean_watercontent_af, peri.sp) mean_soluteconcent_af = np.average(solutecontents_af, weights=np.multiply(mean_watercontent_af, volumes)) - mean_soluteconcent_sr_simp_af = np.average(solutes_sr_simp[1,r-1,2:], weights=np.multiply(watercontent_af, volumes))+(f_root*solutes_sr_simp[1,r-1,0])*dt + mean_soluteconcent_sr_simp_af = solutes_sr_simp[1,r-1,1]+(f_root*solutes_sr_simp[1,r-1,0])*dt + solutes_sr_simp[1,r,1] = mean_soluteconcent_sr_simp_af #Dirichlet BC mean_watercontent_d = np.average(watercontent_d, weights=volumes) mean_waterpotential_d = vg.pressure_head(mean_watercontent_d, peri.sp) mean_soluteconcent_d = np.average(solutecontents_d, weights=np.multiply(mean_watercontent_d, volumes)) - mean_soluteconcent_sr_simp_d = np.average(solutes_sr_simp[2,r-1,2:], weights=np.multiply(watercontent_d, volumes))+(f_root*solutes_sr_simp[2,r-1,0]+f_prhiz*(IC-solutes_sr_simp[2,r-1,-1]))*dt + mean_soluteconcent_sr_simp_d = solutes_sr_simp[2,r-1,1]+(f_root*solutes_sr_simp[2,r-1,0]+f_prhiz*(IC-solutes_sr_simp[2,r-1,-1]))*dt + solutes_sr_simp[2,r,1] = mean_soluteconcent_sr_simp_d print("mean", mean_soluteconcent_sr_simp_d, mean_soluteconcent_sr_simp_nf, f_root, dt, (f_root*solutes_sr_simp[0,r-1,0])*dt) #determine coefficients for the analytical approximation #equation [4] in Schroeder2008 doi:10.2136/vzj2007.0114 @@ -438,14 +441,23 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu # safe the means, they are computed via the explicit Euler timestepping scheme # TODO: technically the concentrations of the last timestep and the current watercontents are mixed, shoul dnot be an issue for small timesteps - mean_soluteconcent_ss_af = np.average(solutes_ss[1,r-1,2:], weights=np.multiply(mean_watercontent_af, volumes))+f_root*solutes_ss[1,r-1,0]*dt #TODO: add inflow from outside - mean_soluteconcent_ss_d = np.average(solutes_ss[2,r-1,2:], weights=np.multiply(mean_watercontent_d, volumes))+(f_root*solutes_ss[2,r-1,0]+f_prhiz*(IC-solutes_ss[2,r-1,-1]))*dt - mean_soluteconcent_sr_af = np.average(solutes_sr[1,r-1,2:], weights=np.multiply(mean_watercontent_af, volumes))+f_root*solutes_sr[1,r-1,0]*dt - mean_soluteconcent_sr_d = np.average(solutes_sr[2,r-1,2:], weights=np.multiply(mean_watercontent_d, volumes))+f_root*solutes_sr[2,r-1,0]*dt - mean_soluteconcent_d_d = np.average(solutes_d[2,r-1,2:], weights=np.multiply(mean_watercontent_d, volumes))+(f_root*solutes_d[2,r-1,0]+f_prhiz*(IC-solutes_d[1,r-1,-1]))*dt - mean_soluteconcent_u_nf = np.average(solutes_u[0,r-1,2:], weights=np.multiply(mean_watercontent_nf, volumes))+f_root*solutes_u[0,r-1,0]*dt - mean_soluteconcent_u_af = np.average(solutes_u[1,r-1,2:], weights=np.multiply(mean_watercontent_af, volumes))+f_root*solutes_u[1,r-1,0]*dt - mean_soluteconcent_u_d = np.average(solutes_u[2,r-1,2:], weights=np.multiply(mean_watercontent_d, volumes))+(f_root*solutes_u[2,r-1,0]+f_prhiz*(IC-solutes_u[2,r-1,-1]))*dt + mean_soluteconcent_ss_af = solutes_ss[1,r-1,1]+f_root*solutes_ss[1,r-1,0]*dt #TODO: add inflow from outside + mean_soluteconcent_ss_d = solutes_ss[2,r-1,1]+(f_root*solutes_ss[2,r-1,0]+f_prhiz*(IC-solutes_ss[2,r-1,-1]))*dt + mean_soluteconcent_sr_af = solutes_sr[1,r-1,1]+f_root*solutes_sr[1,r-1,0]*dt + mean_soluteconcent_sr_d = solutes_sr[2,r-1,1]+f_root*solutes_sr[2,r-1,0]*dt + mean_soluteconcent_d_d = solutes_d[2,r-1,1]+(f_root*solutes_d[2,r-1,0]+f_prhiz*(IC-solutes_d[1,r-1,-1]))*dt + mean_soluteconcent_u_nf = solutes_u[0,r-1,1]+f_root*solutes_u[0,r-1,0]*dt + mean_soluteconcent_u_af = solutes_u[1,r-1,1]+f_root*solutes_u[1,r-1,0]*dt + mean_soluteconcent_u_d = solutes_u[2,r-1,1]+(f_root*solutes_u[2,r-1,0]+f_prhiz*(IC-solutes_u[2,r-1,-1]))*dt + + solutes_ss[1,r,1] = mean_soluteconcent_ss_af + solutes_ss[2,r,1] = mean_soluteconcent_ss_d + solutes_sr[1,r,1] = mean_soluteconcent_sr_af + solutes_sr[2,r,1] = mean_soluteconcent_sr_d + solutes_d[2,r,1] = mean_soluteconcent_d_d + solutes_u[0,r,1] = mean_soluteconcent_u_nf + solutes_u[1,r,1] = mean_soluteconcent_u_af + solutes_u[2,r,1] = mean_soluteconcent_u_d #for the steady state take again the outer concentration #general steady state From 1ebed6983bdf87915b894aaff0bbe6b69f582976 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Tue, 14 Jul 2026 14:54:34 +0200 Subject: [PATCH 39/41] prep for merge --- tutorial/chapter7_coupled/results/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tutorial/chapter7_coupled/results/.gitignore b/tutorial/chapter7_coupled/results/.gitignore index ca49de51a..33498976f 100644 --- a/tutorial/chapter7_coupled/results/.gitignore +++ b/tutorial/chapter7_coupled/results/.gitignore @@ -1,2 +1,4 @@ *.vtk *.vtu +*.npy +*.npz \ No newline at end of file From 626818f9d0c7f25eb822b5156081e543e6506458 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Tue, 14 Jul 2026 14:56:43 +0200 Subject: [PATCH 40/41] prep for merge --- .../test_perirhizal.py | 246 ++++++++++++------ src/functional/Perirhizal.py | 17 +- 2 files changed, 172 insertions(+), 91 deletions(-) diff --git a/experimental/analytical_model_perirhizal/test_perirhizal.py b/experimental/analytical_model_perirhizal/test_perirhizal.py index 474e8caa9..38ed2d65b 100644 --- a/experimental/analytical_model_perirhizal/test_perirhizal.py +++ b/experimental/analytical_model_perirhizal/test_perirhizal.py @@ -35,11 +35,13 @@ # run the dumux implementation of root water and nitrate uptake, later compare it to the analytical approximation n_tests = 1 #try everything here for this many random parameter sets -do_computation = True #should the computation be run or take the data from a saved file +do_computation = False #should the computation be run or take the data from a saved file +showuniform = True #should the uniform implementation be shown +showTiina = True #should Tiinas implementation be shown # general parameters -max_time = 1 # d -n_times = 100 # number of time intervals +max_time = 10 # d +n_times = 1000+1 # number of time slots, -1 for intervals r_prhiz = 0.6 # perirhizal radius[cm], computed for a RLD above 1cm/cm3 r_root = 0.02 # root radius [cm] NC = 10 # number of spatial discretisations @@ -49,8 +51,8 @@ n_scenarios = 3 #initial conditions -initial_waterpotential = -200 -initial_soluteconcentration = 2e-6#mol/cm3 +initial_waterpotential = -100 +initial_soluteconcentration = 1.6e-6#mol/cm3, 103mg/L of NO3 (one of the Tereno measurements in 2015, TODO: look for another source) leads to slightly above 1.6 #space for the outputs # number of tests, number of scenarios, number of timesteps, (rootuptake, inflow, discretisation) @@ -78,6 +80,9 @@ solutes_d = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) #assume a uniform solute concentration solutes_u = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) +#Tiina Roose +solutes_TR = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) + #discretisation lb = 0.5 @@ -85,8 +90,8 @@ CC = np.array([(points[i] + points[i+1])/2 for i in range(NC)]) volumes = np.array([(points[i+1]**2 - points[i]**2)*3.14 for i in range(NC)]) - - +soilVG = [0.078, 0.43, 0.036, 1.56, 24.96] # hydrus loam soil +sp = vg.Parameters(soilVG) def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volumes, length, n_scenarios, initial_waterpotential, initial_soluteconcentration): @@ -102,6 +107,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_ff = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) solutes_d = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) solutes_u = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) + solutes_TR = np.zeros((n_tests, n_scenarios, n_times, NC+2)) #solute concentration in mol/cm3, uptake and inflow in mol/(cm2d) simtimes = np.linspace(0,max_time,n_times)#[1:] dt = max_time / n_times @@ -112,7 +118,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu # root conductivity and solute uptake parameters, constant throughout the entire simulation time molarMassWater = 18 #g/mol - molarMassSolute = 62 #g/mol + molarMassSolute = 62 #g/mol, NO3 root_conductivity = 1e-4 #1/d inner_kr = root_conductivity * r_root * 2 * 3.14 waterdemand = -0.05 #cm/d @@ -189,6 +195,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #steady rate no flux outer BC solute flow in the general water flow solutes_sr = np.zeros((n_scenarios, n_times, NC+2)) + solutes_sr[0,0,1]= initial_soluteconcentration solutes_sr[1,0,1]= initial_soluteconcentration solutes_sr[2,0,1]= initial_soluteconcentration @@ -202,6 +209,8 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_u[1,0,1]= initial_soluteconcentration solutes_u[2,0,1]= initial_soluteconcentration + #Tiina Roose approximation + solutes_TR = np.zeros((n_scenarios, n_times, NC+2)) #variables for the no flux case mean_watercontent_nf = 0.1 #cm3/cm3 @@ -291,15 +300,15 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu rootuptake_w_nf = rootuptake_w_nf[0] rootuptake_s_nf = rootuptake_s_nf[0] - inflow_w_nf = inflow_w_nf[0] + inflow_w_nf = abs(inflow_w_nf[0]) inflow_s_nf = inflow_s_nf[0] rootuptake_w_af = rootuptake_w_af[0] rootuptake_s_af = rootuptake_s_af[0] - inflow_w_af = inflow_w_af[0] + inflow_w_af = abs(inflow_w_af[0]) inflow_s_af = inflow_s_af[0] rootuptake_w_d = rootuptake_w_d[0] rootuptake_s_d = rootuptake_s_d[0] - inflow_w_d = inflow_w_d[0] + inflow_w_d = abs(inflow_w_d[0]) inflow_s_d = inflow_s_d[0] #store the dumux outputs @@ -357,7 +366,6 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu mean_soluteconcent_d = np.average(solutecontents_d, weights=np.multiply(mean_watercontent_d, volumes)) mean_soluteconcent_sr_simp_d = solutes_sr_simp[2,r-1,1]+(f_root*solutes_sr_simp[2,r-1,0]+f_prhiz*(IC-solutes_sr_simp[2,r-1,-1]))*dt solutes_sr_simp[2,r,1] = mean_soluteconcent_sr_simp_d - print("mean", mean_soluteconcent_sr_simp_d, mean_soluteconcent_sr_simp_nf, f_root, dt, (f_root*solutes_sr_simp[0,r-1,0])*dt) #determine coefficients for the analytical approximation #equation [4] in Schroeder2008 doi:10.2136/vzj2007.0114 #Phi(r)=Phi_outer + (q_root*r_root-q_out*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-ln(r/r_prhiz))+q_out*r_prhiz*ln(r/r_prhiz) @@ -377,7 +385,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu Phi_soil_d = vg.fast_mfp[peri.sp](mean_waterpotential_d) #mfp of the mean soil Phi_outer_d = Phi_soil_d - (rootuptake_w_d*r_root-inflow_w_d*r_prhiz)*(rho**2)/(1-rho**2)*(((0.53)**2-1)/2-np.log(0.53))-(inflow_w_d*r_prhiz)*np.log(0.53) #mfp at the outer perirhizal radius Phi_d = lambda r: Phi_outer_d + (rootuptake_w_d*r_root-inflow_w_d*r_prhiz)*(rho**2)/(1-rho**2)*(((r/r_prhiz)**2-1)/2-np.log(r/r_prhiz))+(inflow_w_d*r_prhiz)*np.log(r/r_prhiz)#mfp function depending on radius - Phi_root_d = Phi_nf(r_root)#mfp next to the root + Phi_root_d = Phi_d(r_root)#mfp next to the root #write the steady rate approximations for the water as outputs waterpotential_sr[0,r,0] = rootuptake_w_nf @@ -402,11 +410,12 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu watercontent_sr[2,r,2:] = np.array([vg.water_content(waterpotential_sr[2,r,2+i],peri.sp) for i in range(NC)]) + #compute the analytical approximations for the solute uptake #case of dumux no flux outer BC - result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_nf], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 10) - result_solutes_sr_af = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_af], [Phi_soil_af], [r_root], [r_prhiz], [mean_soluteconcent_af], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 10) - result_solutes_sr_d = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_d], [Phi_soil_d], [r_root], [r_prhiz], [mean_soluteconcent_d], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 10) + result_solutes_sr_nf = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_nf], [Phi_soil_nf], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_nf], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 5) + result_solutes_sr_af = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_af], [Phi_soil_af], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_af], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 5) + result_solutes_sr_d = peri.soil_root_solutes_steadyrate_simplified_([Phi_root_d], [Phi_soil_d], [r_root], [r_prhiz], [mean_soluteconcent_sr_simp_d], [Vmax_per_area], [Km], Ds, [waterdemand], peri.sp, n_approx = 5) #safe the results #(here there is no computed inflow, so index 1 doesn't do anything) @@ -416,11 +425,10 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_sr_simp[1,r,0]=-Vmax_per_area * result_solutes_sr_af / (Km + result_solutes_sr_af) result_solutes_sr_d = result_solutes_sr_d[0] solutes_sr_simp[2,r,0]=-Vmax_per_area * result_solutes_sr_d / (Km + result_solutes_sr_d) - print("Uptake", solutes_sr_simp[2,r,0], result_solutes_sr_d) - F0_nf = peri.integral_AdvectionDiffusion_(Phi_root_nf,peri.sp) - F0_af = peri.integral_AdvectionDiffusion_(Phi_root_af,peri.sp) - F0_d = peri.integral_AdvectionDiffusion_(Phi_root_d,peri.sp) + F0_nf = peri.integral_AdvectionDiffusion_(Phi_nf(CC[0]),peri.sp) + F0_af = peri.integral_AdvectionDiffusion_(Phi_af(CC[0]),peri.sp) + F0_d = peri.integral_AdvectionDiffusion_(Phi_d(CC[0]),peri.sp) D_tilde = 1/Ds/math.pow(sp.theta_S-sp.theta_R,13/3)*(sp.theta_S*sp.theta_S) # the ratio of waterflow and soluteflow is assumed to remain constant throughout the perirhizal zone @@ -429,29 +437,48 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu F_nf = peri.integral_AdvectionDiffusion_(Phi_nf(r_current),peri.sp)-F0_nf F_af = peri.integral_AdvectionDiffusion_(Phi_af(r_current),peri.sp)-F0_af F_d = peri.integral_AdvectionDiffusion_(Phi_d(r_current),peri.sp)-F0_d - F_tilde_nf=math.exp(D_tilde*F_nf) - F_tilde_af=math.exp(D_tilde*F_af) - F_tilde_d=math.exp(D_tilde*F_d) - solutes_sr_simp[0,r,2+j] = result_solutes_sr_nf / F_tilde_nf + (1-1/F_tilde_nf) * solutes_sr_simp[0,r,0] / (waterdemand) - solutes_sr_simp[1,r,2+j] = result_solutes_sr_af / F_tilde_af + (1-1/F_tilde_af) * solutes_sr_simp[1,r,0] / (waterdemand) - solutes_sr_simp[2,r,2+j] = result_solutes_sr_d / F_tilde_d + (1-1/F_tilde_d) * solutes_sr_simp[2,r,0] / (waterdemand) + F_tilde_nf=math.exp(-D_tilde*F_nf) + F_tilde_af=math.exp(-D_tilde*F_af) + F_tilde_d=math.exp(-D_tilde*F_d) + solutes_sr_simp[0,r,2+j] = result_solutes_sr_nf / F_tilde_nf - (1-1/F_tilde_nf) * solutes_sr_simp[0,r,0] / (waterdemand) #rewrite this using water solutes disc + solutes_sr_simp[1,r,2+j] = result_solutes_sr_af / F_tilde_af - (1-1/F_tilde_af) * solutes_sr_simp[1,r,0] / (waterdemand) + solutes_sr_simp[2,r,2+j] = result_solutes_sr_d / F_tilde_d - (1-1/F_tilde_d) * solutes_sr_simp[2,r,0] / (waterdemand) + + #Tiina Roose approximation + E = 0 #minimal solute uptake + DS_TR = Ds * math.pow(mean_watercontent_nf,10/3)/(sp.theta_S**2) + waterflow_TR = 2*np.pi*r_root*waterdemand + waterflow_TR = waterdemand + rsc = peri.solutesuptake_convdiff_([mean_watercontent_nf], [initial_soluteconcentration], [Vmax_per_area], [Km], DS_TR, [waterflow_TR], [r_root], [E], [simtimes[r]], sp) + solutes_TR[0,r,0]=-Vmax_per_area*rsc[0]/(Km + rsc[0]) + solutes_TR[0,r,2]=rsc[0] + + DS_TR = Ds * math.pow(mean_watercontent_af,10/3)/(sp.theta_S**2) + rsc = peri.solutesuptake_convdiff_([mean_watercontent_af], [initial_soluteconcentration], [Vmax_per_area], [Km], DS_TR, [waterflow_TR], [r_root], [E], [simtimes[r]], sp) + solutes_TR[1,r,0]=-Vmax_per_area*rsc[0]/(Km + rsc[0]) + solutes_TR[1,r,2]=rsc[0] + DS_TR = Ds * math.pow(mean_watercontent_d,10/3)/(sp.theta_S**2) + rsc = peri.solutesuptake_convdiff_([mean_watercontent_d], [initial_soluteconcentration], [Vmax_per_area], [Km], DS_TR, [waterflow_TR], [r_root], [E], [simtimes[r]], sp) + solutes_TR[2,r,0]=-Vmax_per_area*rsc[0]/(Km + rsc[0]) + solutes_TR[2,r,2]=rsc[0] # case of general steady rate water uptake # safe the means, they are computed via the explicit Euler timestepping scheme - # TODO: technically the concentrations of the last timestep and the current watercontents are mixed, shoul dnot be an issue for small timesteps - mean_soluteconcent_ss_af = solutes_ss[1,r-1,1]+f_root*solutes_ss[1,r-1,0]*dt #TODO: add inflow from outside + mean_soluteconcent_ss_af = solutes_ss[1,r-1,1]+f_root*solutes_ss[1,r-1,0]*dt mean_soluteconcent_ss_d = solutes_ss[2,r-1,1]+(f_root*solutes_ss[2,r-1,0]+f_prhiz*(IC-solutes_ss[2,r-1,-1]))*dt + mean_soluteconcent_sr_nf = solutes_sr[0,r-1,1]+f_root*solutes_sr[0,r-1,0]*dt mean_soluteconcent_sr_af = solutes_sr[1,r-1,1]+f_root*solutes_sr[1,r-1,0]*dt mean_soluteconcent_sr_d = solutes_sr[2,r-1,1]+f_root*solutes_sr[2,r-1,0]*dt - mean_soluteconcent_d_d = solutes_d[2,r-1,1]+(f_root*solutes_d[2,r-1,0]+f_prhiz*(IC-solutes_d[1,r-1,-1]))*dt + mean_soluteconcent_d_d = solutes_d[2,r-1,1]+(f_root*solutes_d[2,r-1,0]+f_prhiz*(IC-solutes_d[2,r-1,-1]))*dt mean_soluteconcent_u_nf = solutes_u[0,r-1,1]+f_root*solutes_u[0,r-1,0]*dt mean_soluteconcent_u_af = solutes_u[1,r-1,1]+f_root*solutes_u[1,r-1,0]*dt mean_soluteconcent_u_d = solutes_u[2,r-1,1]+(f_root*solutes_u[2,r-1,0]+f_prhiz*(IC-solutes_u[2,r-1,-1]))*dt solutes_ss[1,r,1] = mean_soluteconcent_ss_af solutes_ss[2,r,1] = mean_soluteconcent_ss_d + solutes_sr[0,r,1] = mean_soluteconcent_sr_nf solutes_sr[1,r,1] = mean_soluteconcent_sr_af solutes_sr[2,r,1] = mean_soluteconcent_sr_d solutes_d[2,r,1] = mean_soluteconcent_d_d @@ -464,29 +491,37 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_af], [rootuptake_w_af*2*np.pi*r_root], [inflow_w_af*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_af], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_af, 2*np.pi * rootuptake_w_af*r_root, 2*np.pi * inflow_w_af*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) solutes_ss[1,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_ss[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + #solutes_ss[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_ss[1,r,2:] = soluteconcentration[:] rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "ss") _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_d, 2*np.pi * rootuptake_w_d*r_root, 2*np.pi * inflow_w_d*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) solutes_ss[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_ss[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + #solutes_ss[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_ss[2,r,2:] = soluteconcentration[:] #general steady rate no flux outer BC + rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_nf], [rootuptake_w_nf*2*np.pi*r_root], [inflow_w_nf*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_nf], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") + _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_nf, 2*np.pi * rootuptake_w_nf*r_root, 2*np.pi * inflow_w_nf*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + #print("rsc_sr_nf", rsc, Uptake, quadratic_flow, c_noflux) + solutes_sr[0,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) + #solutes_sr[0,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + solutes_sr[0,r,2:] = soluteconcentration[:] rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_af], [rootuptake_w_af*2*np.pi*r_root], [inflow_w_af*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_af], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_af, 2*np.pi * rootuptake_w_af*r_root, 2*np.pi * inflow_w_af*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + #print("rsc_sr_af", rsc, Uptake, quadratic_flow, c_noflux) solutes_sr[1,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_sr[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + #solutes_sr[1,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_sr[1,r,2:] = soluteconcentration[:] rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "sr") _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_d, 2*np.pi * rootuptake_w_d*r_root, 2*np.pi * inflow_w_d*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) + #print("rsc_sr_d", rsc, Uptake, quadratic_flow, c_noflux) solutes_sr[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_sr[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + #solutes_sr[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_sr[2,r,2:] = soluteconcentration[:] #Dirichlet BC rsc, Uptake, quadratic_flow, c_noflux = peri.soil_root_solutes_sr([Phi_outer_d], [rootuptake_w_d*2*np.pi*r_root], [inflow_w_d*2*np.pi*r_prhiz], [CC[0]], [CC[-1]], [mean_soluteconcent_d], [initial_soluteconcentration], [Vmax], [Km], [Ds], peri.sp, mode = "dirichlet") _, _, soluteconcentration, soluteconcentration_mean = peri.watersolutes_disc(Phi_outer_d, 2*np.pi * rootuptake_w_d*r_root, 2*np.pi * inflow_w_d*r_prhiz, CC[0], CC[-1], CC, Ds, Uptake, quadratic_flow, c_noflux, peri.sp) solutes_d[2,r,0] = -(Uptake[0]+r_root**2*quadratic_flow[0]) / (2 * np.pi * r_root) - solutes_d[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) + #solutes_d[2,r,1] = -(Uptake[0] + r_prhiz**2 * quadratic_flow[0]) solutes_d[2,r,2:] = soluteconcentration[:] #uniform concentration solutes_u[0,r,0] = -Vmax_per_area*mean_soluteconcent_u_nf/(Km + mean_soluteconcent_u_nf) @@ -497,12 +532,12 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_u[2,r,2:] = np.ones(NC) * mean_soluteconcent_u_d - return watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_sr_simp, solutes_ss, solutes_sr, solutes_d, solutes_u + return watercontent_dumux, waterpotential_dumux, watercontent_sr, waterpotential_sr, solutes_dumux, solutes_sr_simp, solutes_ss, solutes_sr, solutes_d, solutes_u, solutes_TR if do_computation: # save everything in the np arrays for i in range(n_tests): - watercontent_dumux[i,:,:,:], waterpotential_dumux[i,:,:,:], watercontent_sr[i,:,:,:], waterpotential_sr[i,:,:,:], solutes_dumux[i,:,:,:], solutes_sr_simp[i,:,:,:], solutes_ss[i,:,:,:], solutes_sr[i,:,:,:], solutes_d[i,:,:,:], solutes_u[i,:,:,:] = run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volumes, length, n_scenarios, initial_waterpotential, initial_soluteconcentration) + watercontent_dumux[i,:,:,:], waterpotential_dumux[i,:,:,:], watercontent_sr[i,:,:,:], waterpotential_sr[i,:,:,:], solutes_dumux[i,:,:,:], solutes_sr_simp[i,:,:,:], solutes_ss[i,:,:,:], solutes_sr[i,:,:,:], solutes_d[i,:,:,:], solutes_u[i,:,:,:], solutes_TR[i,:,:,:] = run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volumes, length, n_scenarios, initial_waterpotential, initial_soluteconcentration) np.savez("test_perirhizal.npz", watercontent_dumux=watercontent_dumux, waterpotential_dumux=waterpotential_dumux, @@ -513,7 +548,8 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_ss=solutes_ss, solutes_sr=solutes_sr, solutes_d=solutes_d, - solutes_u=solutes_u) + solutes_u=solutes_u, + solutes_TR=solutes_TR) else: simulation_results = np.load("test_perirhizal.npz") @@ -527,6 +563,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_sr = simulation_results["solutes_sr"] solutes_d = simulation_results["solutes_d"] solutes_u = simulation_results["solutes_u"] + solutes_TR = simulation_results["solutes_TR"] @@ -576,6 +613,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu solutes_ss_af = solutes_ss[run, 1, timestep[i], 2:] solutes_ss_d = solutes_ss[run, 2, timestep[i], 2:] + solutes_sr_nf = solutes_sr[run, 0, timestep[i], 2:] solutes_sr_af = solutes_sr[run, 1, timestep[i], 2:] solutes_sr_d = solutes_sr[run, 2, timestep[i], 2:] @@ -591,26 +629,27 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu ax1[i,0].plot(CC, water_steadyrate_nf, "b", linestyle = linestyle_steadyrate, label = "water_sr") ax2_0.plot(CC, solute_dumux_nf, "m", linestyle = linestyle_dumux, label = "solute_dumux") ax2_0.plot(CC, solutes_sr_simp_nf, "m", linestyle = linestyle_steadyrate, label = "solute_sr_simp") - ax2_0.plot(CC, solutes_u_nf, "m", linestyle = linestyle_dumux, label = "solute_u") + ax2_0.plot(CC, solutes_sr_nf, "g", linestyle = linestyle_steadyrate, label = "solute_sr") + ax2_0.plot(CC, solutes_u_nf, "r", linestyle = linestyle_dumux, label = "solute_u") #middle plot: water dirichlet, advective flow solutes ax1[i,1].plot(CC, water_dumux_af, "b", linestyle = linestyle_dumux, label = "water_dumux") ax1[i,1].plot(CC, water_steadyrate_af, "b", linestyle = linestyle_steadyrate, label = "water_sr") ax2_1.plot(CC, solute_dumux_af, "m", linestyle = linestyle_dumux, label = "solute_dumux") ax2_1.plot(CC, solutes_sr_simp_af, "m", linestyle = linestyle_steadyrate, label = "solute_sr_simp") - ax2_1.plot(CC, solutes_sr_af, "m", linestyle = linestyle_steadyrate, label = "solute_sr") - ax2_1.plot(CC, solutes_ss_af, "m", linestyle = linestyle_steadystate, label = "solute_ss") - ax2_1.plot(CC, solutes_u_af, "m", linestyle = linestyle_dumux, label = "solute_u") + ax2_1.plot(CC, solutes_sr_af, "g", linestyle = linestyle_steadyrate, label = "solute_sr") + ax2_1.plot(CC, solutes_ss_af, "y", linestyle = linestyle_steadystate, label = "solute_ss") + ax2_1.plot(CC, solutes_u_af, "r", linestyle = linestyle_dumux, label = "solute_u") #right plot: Dirichlet (initial conditions) outer BC ax1[i,2].plot(CC, water_dumux_d, "b", linestyle = linestyle_dumux, label = "water_dumux") ax1[i,2].plot(CC, water_steadyrate_d, "b", linestyle = linestyle_steadyrate, label = "water_sr") ax2_2.plot(CC, solute_dumux_d, "m", linestyle = linestyle_dumux, label = "solute_dumux") ax2_2.plot(CC, solutes_sr_simp_d, "m", linestyle = linestyle_steadyrate, label = "solute_sr_simp") - ax2_2.plot(CC, solutes_sr_d, "m", linestyle = linestyle_steadyrate, label = "solute_sr") - ax2_2.plot(CC, solutes_ss_d, "m", linestyle = linestyle_steadystate, label = "solute_ss") - ax2_2.plot(CC, solutes_d_d, "m", linestyle = linestyle_special, label = "solute_d") - ax2_2.plot(CC, solutes_u_d, "m", linestyle = linestyle_dumux, label = "solute_u") + ax2_2.plot(CC, solutes_sr_d, "g", linestyle = linestyle_steadyrate, label = "solute_sr") + ax2_2.plot(CC, solutes_ss_d, "y", linestyle = linestyle_steadystate, label = "solute_ss") + ax2_2.plot(CC, solutes_d_d, "c", linestyle = linestyle_special, label = "solute_d") + ax2_2.plot(CC, solutes_u_d, "r", linestyle = linestyle_dumux, label = "solute_u") ax1[i,0].set_xlabel("distance root [cm]") ax1[i,0].set_ylabel("water") @@ -640,6 +679,9 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu ax2_2.legend(loc="upper right") #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +figure = plt.gcf() # get current figure +figure.set_size_inches(8, 6) +plt.savefig("concentrations", dpi = 100) plt.show() @@ -661,6 +703,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu suptake_ss_af = solutes_ss[run, 1, 1:, 0] suptake_ss_d = solutes_ss[run, 2, 1:, 0] +suptake_sr_nf = solutes_sr[run, 0, 1:, 0] suptake_sr_af = solutes_sr[run, 1, 1:, 0] suptake_sr_d = solutes_sr[run, 2, 1:, 0] @@ -670,26 +713,34 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu suptake_u_af = solutes_u[run, 1, 1:, 0] suptake_u_d = solutes_u[run, 2, 1:, 0] +suptake_TR_nf = solutes_TR[run, 0, 1:, 0] +suptake_TR_af = solutes_TR[run, 1, 1:, 0] +suptake_TR_d = solutes_TR[run, 2, 1:, 0] + ax1[0].plot(suptake_dumux_nf, suptake_dumux_nf, "m", linestyle = linestyle_dumux, label = "dumux") -ax1[0].scatter(suptake_dumux_nf, abs(suptake_dumux_nf), marker = "*") -ax1[0].plot(suptake_dumux_nf, abs(suptake_sr_simp_nf), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") -ax1[0].plot(suptake_dumux_nf, abs(suptake_u_nf), "g", linestyle = linestyle_steadyrate, label = "uniform") +#ax1[0].scatter(suptake_dumux_nf, abs(suptake_dumux_nf), "m", marker = "*") +ax1[0].plot(suptake_dumux_nf, abs(suptake_sr_simp_nf), "b", linestyle = linestyle_dumux, label = "steady rate simp") +ax1[0].plot(suptake_dumux_nf, abs(suptake_sr_nf), "g", linestyle = linestyle_dumux, label = "steady rate") +ax1[0].plot(suptake_dumux_nf, abs(suptake_u_nf), "r", linestyle = linestyle_dumux, label = "uniform") +ax1[0].plot(suptake_dumux_nf, abs(suptake_TR_nf), "k", linestyle = linestyle_dumux, label = "TR") ax1[1].plot(suptake_dumux_af, suptake_dumux_af, "m", linestyle = linestyle_dumux, label = "dumux") -ax1[1].scatter(suptake_dumux_af, abs(suptake_dumux_af), marker = "*") -ax1[1].plot(suptake_dumux_af, abs(suptake_sr_simp_af), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") -ax1[1].plot(suptake_dumux_af, abs(suptake_sr_af), "p", linestyle = linestyle_steadyrate, label = "steady rate") -ax1[1].plot(suptake_dumux_af, abs(suptake_ss_af), "v", linestyle = linestyle_steadystate, label = "steady state") -ax1[1].plot(suptake_dumux_af, abs(suptake_u_af), "g", linestyle = linestyle_steadyrate, label = "uniform") +#ax1[1].scatter(suptake_dumux_af, abs(suptake_dumux_af), marker = "*") +ax1[1].plot(suptake_dumux_af, abs(suptake_sr_simp_af), "b", linestyle = linestyle_dumux, label = "steady rate simp") +ax1[1].plot(suptake_dumux_af, abs(suptake_sr_af), "g", linestyle = linestyle_dumux, label = "steady rate") +ax1[1].plot(suptake_dumux_af, abs(suptake_ss_af), "y", linestyle = linestyle_dumux, label = "steady state") +ax1[1].plot(suptake_dumux_af, abs(suptake_u_af), "r", linestyle = linestyle_dumux, label = "uniform") +ax1[1].plot(suptake_dumux_af, abs(suptake_TR_af), "k", linestyle = linestyle_dumux, label = "TR") ax1[2].plot(suptake_dumux_d, suptake_dumux_d, "m", linestyle = linestyle_dumux, label = "dumux") -ax1[2].scatter(suptake_dumux_d, abs(suptake_dumux_d), marker = "*") -ax1[2].plot(suptake_dumux_d, abs(suptake_sr_simp_d), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") -ax1[2].plot(suptake_dumux_d, abs(suptake_sr_d), "p", linestyle = linestyle_steadyrate, label = "steady rate") -ax1[2].plot(suptake_dumux_d, abs(suptake_ss_d), "v", linestyle = linestyle_steadystate, label = "steady state") -ax1[2].plot(suptake_dumux_d, abs(suptake_d_d), "c", linestyle = linestyle_special, label = "steady state") -ax1[2].plot(suptake_dumux_d, abs(suptake_u_d), "g", linestyle = linestyle_steadyrate, label = "uniform") +#ax1[2].scatter(suptake_dumux_d, abs(suptake_dumux_d), marker = "*") +ax1[2].plot(suptake_dumux_d, abs(suptake_sr_simp_d), "b", linestyle = linestyle_dumux, label = "steady rate simp") +ax1[2].plot(suptake_dumux_d, abs(suptake_sr_d), "g", linestyle = linestyle_dumux, label = "steady rate") +ax1[2].plot(suptake_dumux_d, abs(suptake_ss_d), "y", linestyle = linestyle_dumux, label = "steady state") +ax1[2].plot(suptake_dumux_d, abs(suptake_d_d), "c", linestyle = linestyle_dumux, label = "steady state") +ax1[2].plot(suptake_dumux_d, abs(suptake_u_d), "r", linestyle = linestyle_dumux, label = "uniform") +ax1[2].plot(suptake_dumux_d, abs(suptake_TR_d), "k", linestyle = linestyle_dumux, label = "TR") ax1[0].set_xlabel("dumux solute uptake") ax1[0].set_ylabel("analytical approximation") @@ -703,7 +754,10 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu ax1[2].set_ylabel("analytical approximation") ax1[2].legend(["solute utake mol/cm2d"], loc="upper left") -#np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +#np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +figure = plt.gcf() # get current figure +figure.set_size_inches(8, 6) +plt.savefig("relative_uptake", dpi = 100) plt.show() @@ -716,6 +770,10 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu #load solute uptake suptake_dumux_nf = solutes_dumux[run, 0, 1:, 0] +#doublecheck if the uptake is correct +beginning_solutes = initial_soluteconcentration * vg.water_content(initial_waterpotential,sp) * (np.pi * (r_prhiz**2 - r_root**2) * length) +total_uptake_nf = [np.sum([solutes_dumux[0, 0, i, 2+j] * watercontent_dumux[0, 0, i, 2+j] * np.pi * (points[j+1]**2-points[j]**2) * length for j in range(NC)]) for i in range(n_times)] +total_uptake_nf = np.array([(beginning_solutes - total_uptake_nf[i])/(2*np.pi*r_root*length)/max_time*(n_times-1) for i in range(n_times)]) suptake_dumux_af = solutes_dumux[run, 1, 1:, 0] suptake_dumux_d = solutes_dumux[run, 2, 1:, 0] @@ -726,6 +784,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu suptake_ss_af = solutes_ss[run, 1, 1:, 0] suptake_ss_d = solutes_ss[run, 2, 1:, 0] +suptake_sr_nf = solutes_sr[run, 0, 1:, 0] suptake_sr_af = solutes_sr[run, 1, 1:, 0] suptake_sr_d = solutes_sr[run, 2, 1:, 0] @@ -746,6 +805,7 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu suptake_ss_af = np.array([ sum(suptake_ss_af[:i]) for i in range(n_times)]) suptake_ss_d = np.array([ sum(suptake_ss_d[:i]) for i in range(n_times)]) +suptake_sr_nf = np.array([ sum(suptake_sr_nf[:i]) for i in range(n_times)]) suptake_sr_af = np.array([ sum(suptake_sr_af[:i]) for i in range(n_times)]) suptake_sr_d = np.array([ sum(suptake_sr_d[:i]) for i in range(n_times)]) @@ -755,36 +815,56 @@ def run_perirhizal_test(max_time, n_times, r_prhiz, r_root, NC, points, CC, volu suptake_u_af = np.array([ sum(suptake_u_af[:i]) for i in range(n_times)]) suptake_u_d = np.array([ sum(suptake_u_d[:i]) for i in range(n_times)]) +suptake_TR_nf = np.array([ sum(suptake_TR_nf[:i]) for i in range(n_times)]) +suptake_TR_af = np.array([ sum(suptake_TR_af[:i]) for i in range(n_times)]) +suptake_TR_d = np.array([ sum(suptake_TR_d[:i]) for i in range(n_times)]) + ax1[0].plot(simtimes, abs(suptake_dumux_nf), "m", linestyle = linestyle_dumux, label = "dumux") -ax1[0].plot(simtimes, abs(suptake_sr_simp_nf), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") -ax1[0].plot(simtimes, abs(suptake_u_nf), "g", linestyle = linestyle_steadyrate, label = "uniform") +#ax1[0].plot(simtimes[1:], abs(total_uptake_nf[1:]), "g", linestyle = linestyle_dumux, label = "dumux 2 uptake") #should produce the same line +ax1[0].plot(simtimes, abs(suptake_sr_simp_nf), "b", linestyle = linestyle_dumux, label = "steady rate simp") +ax1[0].plot(simtimes, abs(suptake_sr_nf), "g", linestyle = linestyle_dumux, label = "steady rate") + ax1[1].plot(simtimes, abs(suptake_dumux_af), "m", linestyle = linestyle_dumux, label = "dumux") -ax1[1].plot(simtimes, abs(suptake_sr_simp_af), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") -ax1[1].plot(simtimes, abs(suptake_sr_af), "p", linestyle = linestyle_steadyrate, label = "steady rate") -ax1[1].plot(simtimes, abs(suptake_ss_af), "v", linestyle = linestyle_steadystate, label = "steady state") -ax1[1].plot(simtimes, abs(suptake_u_af), "g", linestyle = linestyle_steadyrate, label = "uniform") +ax1[1].plot(simtimes, abs(suptake_sr_simp_af), "b", linestyle = linestyle_dumux, label = "steady rate simp") +ax1[1].plot(simtimes, abs(suptake_sr_af), "g", linestyle = linestyle_dumux, label = "steady rate") +ax1[1].plot(simtimes, abs(suptake_ss_af), "y", linestyle = linestyle_dumux, label = "steady state") ax1[2].plot(simtimes, abs(suptake_dumux_d), "m", linestyle = linestyle_dumux, label = "dumux") -ax1[2].plot(simtimes, abs(suptake_sr_simp_d), "b", linestyle = linestyle_steadyrate, label = "steady rate simp") -ax1[2].plot(simtimes, abs(suptake_sr_d), "p", linestyle = linestyle_steadyrate, label = "steady rate") -ax1[2].plot(simtimes, abs(suptake_ss_d), "v", linestyle = linestyle_steadystate, label = "steady state") -ax1[2].plot(simtimes, abs(suptake_d_d), "c", linestyle = linestyle_special, label = "steady state") -ax1[2].plot(simtimes, abs(suptake_u_d), "g", linestyle = linestyle_steadyrate, label = "uniform") - -ax1[0].set_xlabel("dumux solute uptake") -ax1[0].set_ylabel("analytical approximation") -ax1[0].legend(["solute utake mol/cm2d"], loc="upper left") - -ax1[1].set_xlabel("dumux solute uptake") -ax1[1].set_ylabel("analytical approximation") -ax1[1].legend(["solute utake mol/cm2d"], loc="upper left") - -ax1[2].set_xlabel("dumux solute uptake") -ax1[2].set_ylabel("analytical approximation") -ax1[2].legend(["solute utake mol/cm2d"], loc="upper left") +ax1[2].plot(simtimes, abs(suptake_sr_simp_d), "b", linestyle = linestyle_dumux, label = "steady rate simp") +ax1[2].plot(simtimes, abs(suptake_sr_d), "g", linestyle = linestyle_dumux, label = "steady rate nf BC") +ax1[2].plot(simtimes, abs(suptake_ss_d), "y", linestyle = linestyle_dumux, label = "steady state") +ax1[2].plot(simtimes, abs(suptake_d_d), "c", linestyle = linestyle_dumux, label = "steady rate D BC") + +if showuniform: + ax1[0].plot(simtimes, abs(suptake_u_nf), "r", linestyle = linestyle_dumux, label = "uniform") + ax1[1].plot(simtimes, abs(suptake_u_af), "r", linestyle = linestyle_dumux, label = "uniform") + ax1[2].plot(simtimes, abs(suptake_u_d), "r", linestyle = linestyle_dumux, label = "uniform") + +if showTiina: + ax1[0].plot(simtimes, abs(suptake_TR_nf), "k", linestyle = linestyle_dumux, label = "TR") + ax1[1].plot(simtimes, abs(suptake_TR_af), "k", linestyle = linestyle_dumux, label = "TR") + ax1[2].plot(simtimes, abs(suptake_TR_d), "k", linestyle = linestyle_dumux, label = "TR") +#ax1[0].set_xlabel("dumux solute uptake") +#ax1[0].set_ylabel("analytical approximation") +#ax1[0].legend(["solute utake mol/cm2d"], loc="upper left") + +#ax1[1].set_xlabel("dumux solute uptake") +#ax1[1].set_ylabel("analytical approximation") +#ax1[1].legend(["solute utake mol/cm2d"], loc="upper left") + +#ax1[2].set_xlabel("dumux solute uptake") +#ax1[2].set_ylabel("analytical approximation") +#ax1[2].legend(["solute utake mol/cm2d"], loc="upper left") + +ax1[0].legend(loc="upper left") +ax1[1].legend(loc="upper left") +ax1[2].legend(loc="upper left") #np.save("input/" + filename + "_fp", np.vstack((sim_times_, -np.array(t_act_), np.array(q_soil_)))) +figure = plt.gcf() # get current figure +figure.set_size_inches(8, 6) +plt.savefig("cumulative_uptake", dpi = 100) plt.show() diff --git a/src/functional/Perirhizal.py b/src/functional/Perirhizal.py index 44234d376..a235bb36f 100644 --- a/src/functional/Perirhizal.py +++ b/src/functional/Perirhizal.py @@ -664,7 +664,7 @@ def soil_root_solutes_steadyrate_simplified_(self, Phi_root, Phi_soil, r_root, r else: F=[(self.integral_AdvectionDiffusion_(Phi_current[i],self.sp)-F0[i]) for i in range(n_segments)] for i in range(n_segments): - c_sol_mean2root[i] += weights[j] * math.exp(D_tilde*F[i]) * current_watercontent[i] + c_sol_mean2root[i] += weights[j] * math.exp(max(min(D_tilde*F[i],10),-10)) * current_watercontent[i] mean_watercontent[i] += weights[j] * current_watercontent[i] for i in range(n_segments): c_sol_mean2root[i] = c_sol_mean2root[i] / mean_watercontent[i] @@ -877,7 +877,7 @@ def create_lookup(self, filename, sp): base_mfp_interval1= -np.logspace(np.log10(-base_mfp_int[0]), np.log10(1.0e-9), base_mfp_n_1) base_mfp_interval2= np.logspace(np.log10(1.0e-9), np.log10(base_mfp_int[1]), base_mfp_n_2) base_mfp_ = np.concatenate((base_mfp_interval1,base_mfp_interval2)) - + print("boundaries lookup", base_mfp_[0], base_mfp_[1]) interface = np.zeros((inner_kr_bn,base_mfp_n)) print("Creating a lookup table for the hydraulic perirhizal resistance model") @@ -980,16 +980,16 @@ def create_integralAdvectionDiffusion_lookup(self, filename, sp): Phin = 300 Phi_ = np.logspace(np.log10(1.0e-9), np.log10(500), Phin) base_mfp_=Phi_ - integral_overD_ = np.zeros((Phin, 2)) + integral_AdvDiff_ = np.zeros((Phin, 2)) print("Creating a lookup table for the steady state solute flow") for i, Phi in enumerate(Phi_): print(i, " / ", Phin) - integral_overD_[i,0] = PerirhizalPython.integral_overDiffusion_(Phi, sp) - integral_overD_[i,1] = integral_overD_[i,0] + integral_AdvDiff_[i,0] = PerirhizalPython.integral_AdvectionDiffusion_(Phi, sp) + integral_AdvDiff_[i,1] = integral_AdvDiff_[i,0] np.savez(filename, integral_AdvDiff_ = integral_AdvDiff_, base_mfp_ = base_mfp_, soil = list(sp)) - self.lookup_table_solutes = RegularGridInterpolator((base_mfp_,[0,1]) , integral_overD_) + self.lookup_table_sr_solutes_simplified = RegularGridInterpolator((base_mfp_,[0,1]) , integral_AdvDiff_) self.sp = sp - return integral_overD_, base_mfp_ + return integral_AdvDiff_, base_mfp_ def create_solute_sr_lookup(self, filename, sp): """ @@ -1080,12 +1080,13 @@ def soil_root_interface_potentials_table(self, rx, sx, inner_kr_, rho_): #compute two inputs for the lookup table rho = np.array(rho_) + rho[rho > 199] = 199#maximal for rho rho2 = np.multiply(rho,rho) b = 2 * (rho2 - 1) / (1 - 0.53 * 0.53 * rho2 + 2 * rho2 * (np.log(rho) + np.log(0.53))) # Vanderborgth et al. 2023, Eqn [8] inner_kr_b = np.divide(inner_kr_,b) base_mfp = - (inner_kr_b * rx + vg.fast_mfp[self.sp](sx)) - rsx = self.lookup_table((inner_kr_b, np.array([max(min(base_mfp[i],500),-500) for i in range(len(base_mfp))]))) + rsx = self.lookup_table((inner_kr_b, np.array([max(min(base_mfp[i],-51.01),-53.59) for i in range(len(base_mfp))]))) rsx[mask] = sx[mask] # if inner_kr is zero, there is no flow, and the interface potential is the same as the soil potential except: if np.max(rx) > 0: From 24d55fc4f6770bfbd2f9596147cfd263324dbf46 Mon Sep 17 00:00:00 2001 From: Erik Kopp Date: Tue, 14 Jul 2026 15:29:25 +0200 Subject: [PATCH 41/41] fix insertion in PlantHydraulicModel --- src/functional/PlantHydraulicParameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functional/PlantHydraulicParameters.py b/src/functional/PlantHydraulicParameters.py index 193a9ed55..f7305d041 100644 --- a/src/functional/PlantHydraulicParameters.py +++ b/src/functional/PlantHydraulicParameters.py @@ -121,7 +121,7 @@ def read_parameters(self, filename): self.setKxValues(json_dict["kxPerSegment"]) for ot in [int(pb.OrganTypes.root), int(pb.OrganTypes.stem), int(pb.OrganTypes.leaf)]: maxSubTypes_ot = min(self.maxSubTypes, len(json_dict["kx_ages"][str(ot)])) - for st in range(0, maxSubTypes_ot):git + for st in range(0, maxSubTypes_ot): age_ = json_dict["kx_ages"][str(ot)][st] value_ = json_dict["kx_values"][str(ot)][st] self.setKxAgeDependent(age_, value_, st, ot)