import numpy as np
from scipy.optimize import minimize
from pedophysics.utils.stats import R2_score
from pedophysics.pedophysical_models.bulk_ec import Fu, WunderlichEC
from .water_ec import WaterEC
from .porosity import Porosity
from .solid_ec import SolidEC
from .frequency_ec import FrequencyEC
from .texture import Texture
[docs]def WaterFromEC(soil):
"""
Calculate missing values of soil.df.water based on soil.df.bulk_ec_dc_tc
This function evaluates the availability of water content and bulk electrical conductivity data (bulk_ec_dc_tc) across soil states.
A fitting approach is applied if there are at least three soil states with known water content and bulk electrical conductivity.
A non-fitting approach is considered when water content is unknown and bulk electrical conductivity is known for any soil state.
Parameters
----------
soil : Soil Object
An object representing the soil, which must have the following attributes:
- df: DataFrame
Data Frame containing the quantitative information of all soil array-like attributes for each state.
Includes: `water` and `bulk_ec_dc_tc`.
- info: DataFrame
Data Frame containing descriptive information about how each array-like attribute was calculated.
- n_states: int
The number of soil states represented in the `df`.
Returns
-------
None
Notes
-----
- The fitting approach requires at least three soil states with known water content and bulk electrical conductivity for reliable estimation.
- The non-fitting approach is applied to individual soil states where water content is unknown but bulk electrical conductivity is available.
External Functions
------------------
FrequencyEC : Set missing values of soil.df.frequency_ec and return
fitting : Calculate missing values of soil.df.water using a fitting approach.
non_fitting : Calculate missing values of soil.df.water using a non-fitting approach.
Example
-------
>>> sample = Soil( bulk_ec = [0.01, np.nan, 0.025, 0.030, 0.040],
clay = 10,
porosity = 0.47,
water_ec = 0.5)
>>> WaterFromEC(sample)
>>> sample.df.water
0 0.105
1 Nan
2 0.185
3 0.206
4 0.243
Name: water, dtype: float64
"""
FrequencyEC(soil)
# Check for conditions to use a fitting approach
if sum(not np.isnan(soil.water[x]) and not np.isnan(soil.df.bulk_ec_dc_tc[x]) for x in range(soil.n_states)) >= 3:
fitting(soil)
# Check for conditions to use a non-fitting approach
if any(np.isnan(soil.df.water[x]) and not np.isnan(soil.df.bulk_ec_dc_tc[x]) for x in range(soil.n_states)):
non_fitting(soil)
[docs]def non_fitting(soil):
"""
Calculate missing values of soil.df.water using a non-fitting approach.
This function applies the Fu function within a minimization process to estimate soil water content based on soil properties such as
clay content, porosity, water electrical conductivity (EC), solid EC, dry EC, and saturated EC.
The estimation is performed for each soil state where water content is unknown.
Parameters
----------
soil : Soil Object
An object representing the soil, which must have the following attributes:
- df: DataFrame
Data Frame containing the quantitative information of all soil array-like attributes for each state.
Includes: `clay`, `porosity`, `water_ec`, `solid_ec`, `dry_ec`, `sat_ec`, `bulk_ec_dc_tc`, and potentially `water`.
- info: DataFrame
Data Frame containing descriptive information about how each array-like attribute was calculated.
- n_states: int
The number of soil states represented in the `df`.
- roundn: int
The number of decimal places for rounding estimated water content values.
bulk_ec_dc : array-like
Soil bulk real electrical conductivity at DC frequency [S/m].
Notes
-----
- The Fu function is utilized in a minimization process to estimate water content by minimizing the difference between the estimated and actual bulk ECDCTC.
- The estimation process is applied to each soil state where water content is unknown.
External functions
--------
Fu: Calculate the soil bulk real electrical conductivity using the Fu model and return
Texture: Calculate missing values of soil.df.sand, soil.df.silt, and soil.df.clay and return
Porosity: Calculate missing values of soil.df.porosity and return
WaterEC: Compute missing values of soil.df.water_ec and return
SolidEC: Set missing values of soil.df.solid_ec and return
"""
Texture(soil)
Porosity(soil)
WaterEC(soil)
SolidEC(soil)
# Defining minimization function to obtain water using Fu
def objective_func_wat(x, clay, porosity, water_ec, solid_ec, dry_ec, sat_ec, EC):
return (Fu(x, clay, porosity, water_ec, solid_ec, dry_ec, sat_ec) - EC)**2
wat = []
# Calculating water
for i in range(soil.n_states):
res = minimize(objective_func_wat, 0.15, args=(soil.df.clay[i], soil.df.porosity[i], soil.df.water_ec[i], soil.df.solid_ec[i],
soil.df.dry_ec[i], soil.df.sat_ec[i], soil.df.bulk_ec_dc_tc[i]), bounds=[(0, .65)] )
wat.append(np.nan if np.isnan(res.fun) else round(res.x[0], soil.roundn) )
# Check for missing values
missing_water_before = soil.df['water'].isna()
soil.df['water'] = [round(wat[i], soil.roundn) if np.isnan(soil.df.water[i]) else soil.df.water[i] for i in range(soil.n_states) ]
missing_water_after = soil.df['water'].isna()
# Update info for calculated water
soil.info['water'] = [str(soil.info.water[x]) + (
"--> Calculated using Fu function (reported R2=0.98) in predict.water_from_ec.non_fitting"
if missing_water_before[x] and not missing_water_after[x]
else "--> Provide water; otherwise clay, porosity, water_ec and bulk_ec_dc_tc"
if missing_water_before[x] and missing_water_after[x]
else "")
if missing_water_before[x]
else soil.info.water[x]
for x in range(soil.n_states)]
[docs]def fitting(soil):
"""
Calculate missing values of soil.df.water using a fitting approach.
This function evaluates soil states with known water content and bulk electrical conductivity to determine initial parameters for the WunderlichEC model.
If the Lw parameter associated with the model is unknown, it is optimized based on the root mean square error (RMSE) between estimated and actual bulk electrical conductivity.
Water content is then estimated for all soil states within a valid bulk electrical conductivity range using the optimized Lw parameter and the WunderlichEC model.
Parameters
----------
soil : Soil Object
An object representing the soil, which must have the following attributes:
- df: DataFrame
Data Frame containing the quantitative information of all soil array-like attributes for each state.
Includes: `water`, `bulk_ec_dc_tc`, `water_ec`, and potentially `Lw`.
- info: DataFrame
Data Frame containing descriptive information about how each array-like attribute was calculated.
- n_states: int
The number of soil states represented in the `df`.
- range_ratio: float
A ratio used to determine the range of bulk electrical conductivity values considered valid for the model.
- roundn: int
The number of decimal places for rounding calculated water content values.
- Lw: float or np.nan
The WunderlichEC model parameter, if known; otherwise, np.nan.
Returns
-------
None
The function directly modifies the `soil` object's `df` and `info` attributes with the estimated water content and does not return any value.
Notes
-----
This function modifies the soil object in-place by updating the `df` and `info` dataframes.
The function either estimates or uses the known Lw parameter for the WunderlichEC model and
fits the model to the calibration data.
External Functions
------------------
WunderlichEC: Calculate the soil bulk real electrical conductivity using the Wunderlich model and return
WaterEC: Compute missing values of soil.df.water_ec and return
"""
WaterEC(soil)
# Defining model parameters
valids = ~np.isnan(soil.df.water) & ~np.isnan(soil.df.bulk_ec_dc_tc) # States where calibration data are
water_init = np.nanmin(soil.df.water[valids])
bulk_ec_init = np.nanmin(soil.df.bulk_ec_dc_tc[valids])
bulk_ec_final = np.nanmax(soil.df.bulk_ec_dc_tc[valids])
bulk_ec_range = [round(bulk_ec_init - (bulk_ec_final-bulk_ec_init)/soil.range_ratio, soil.roundn),
round(bulk_ec_final + (bulk_ec_final-bulk_ec_init)/soil.range_ratio, soil.roundn)]
if bulk_ec_range[0] < 0:
bulk_ec_range[0] = 0
# Obtain Lw attribute if unknown
if np.isnan(soil.Lw):
# Defining minimization function to obtain water
def objective_Lw(Lw):
wund_eval = [WunderlichEC(soil.df.water[x], bulk_ec_init, water_init, soil.df.water_ec[x], Lw)[0] if valids[x] else np.nan for x in range(soil.n_states)]
Lw_RMSE = np.sqrt(np.nanmean((np.array(wund_eval) - soil.df.bulk_ec_dc_tc)**2))
return Lw_RMSE
# Calculating optimal Lw
result = minimize(objective_Lw, 0.1, bounds=[(-0.2, 0.8)], method='L-BFGS-B')
soil.Lw = result.x[0]
# If Lw is known
if ~np.isnan(soil.Lw):
if not isinstance(soil.Lw, np.floating):
soil.Lw = soil.Lw[0]
Wat_wund = []
# Defining minimization function to obtain water
def objective_wat(wat, i):
Wat_RMSE = np.sqrt((WunderlichEC(wat, bulk_ec_init, water_init, soil.df.water_ec[i], soil.Lw) - soil.df.bulk_ec_dc_tc[i])**2)
return Wat_RMSE
# Looping over soil states to obtain water using WunderlichEC function
for i in range(soil.n_states):
if (min(bulk_ec_range) <= soil.df.bulk_ec_dc_tc[i] <= max(bulk_ec_range)) & ~np.isnan(soil.df.bulk_ec_dc_tc[i]):
result = minimize(objective_wat, 0.15, args=(i), bounds=[(0, .65)], method='L-BFGS-B')
Wat_wund.append(np.nan if np.isnan(result.fun) else round(result.x[0], soil.roundn))
else:
Wat_wund.append(np.nan)
# Calculating the R2 score of the model fitting
R2 = round(R2_score(soil.df.water[valids], np.array(Wat_wund)[valids]), soil.roundn)
missing_water_before = soil.df['water'].isna()
soil.df['water'] = [Wat_wund[x] if np.isnan(soil.df.water[x]) else soil.df.water[x] for x in range(soil.n_states)]
missing_water_after = soil.df['water'].isna()
soil.info['water'] = [str(soil.info.water[x]) + (
"--> Calculated by fitting (R2="+str(R2)+") WunderlichEC function in predict.water_from_ec.fitting, for soil.bulk_ec values between: "+str(bulk_ec_range)
if missing_water_before[x] and not missing_water_after[x]
else "--> Provide water; otherwise, bulk_ec_dc_tc and water_ec. Regression valid for bulk_ec_dc_tc values between: "+str(bulk_ec_range)
if missing_water_before[x] and missing_water_after[x]
else "")
if missing_water_before[x]
else soil.info.water[x]
for x in range(soil.n_states)]