64  Soil moisture drydowns

Author

Andres Patrignani

Published

January 22, 2024

Keywords

soil moisture, drydown, exponential decay

Soil moisture drydowns refer to the rate at which soil loses its moisture content over time, typically following a rainfall event. The initial rate of moisture loss is typically rapid, slowing down as the soil reaches a lower moisture content. Thus, this process is often described by an exponential decay model.

In this exercise we will extract drydown events from a time series of rootzone soil moisture. Basically, a drydown represents the period of moisture loss between precipitation events. Since in this region small rainfall events don’t usually contribute to appreciable soil moisture recharge, we will set a tolerance level to ignore small rainfall events.

Model description

SWC = A \ e^{-t/\tau} + \theta_{res}

SWC = Soil water content in m^{3}/m^{3}
A = The initial soil water content above \theta_{res} in m^{3}/m^{3} (i.e., SWC = A + \theta_{res} at time t=0)
t = Days since rainfall event
\tau = Constant that modulates the rate at which the soil dries
\theta_{res} = Residual soil water content m^{3}/m^{3}.

# Import modules
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
from pprint import pprint
# Define model using an anonymous lambda function
model = lambda t,tau,A,S_min: A * np.exp(-t/tau) + S_min
xrange = np.arange(30)
# Create figure with example drydowns
plt.figure(figsize=(5,4))

# Rapid decay. Typical of summer, coarse soils, and actively growing vegetation
plt.plot(xrange, model(xrange,5,70,50), color='green')     

# Drydowns during moderate atmospheric demand (spring and fall)
plt.plot(xrange, model(xrange,20,70,50), color='tomato')

# Drydown during low atmospheric demand (winter)
plt.plot(xrange, model(xrange,100,70,50), color='navy')

plt.xlabel('Days since last rainfall')
plt.ylabel('Storage (mm)')
plt.show()

Load dataset

# Load data
df = pd.read_csv('../datasets/kings_creek_2022_2023_daily.csv',parse_dates=['datetime'])
df.head()
datetime pressure tmin tmax tavg rmin rmax prcp srad wspd wdir vpd vwc_5cm vwc_20cm vwc_40cm soiltemp_5cm soiltemp_20cm soiltemp_40cm battv discharge
0 2022-01-01 96.838 -14.8 -4.4 -9.60 78.475 98.012 0.25 2.098 5.483 0.969 0.028 0.257 0.307 0.359 2.996 5.392 7.425 8714.833 0.0
1 2022-01-02 97.995 -20.4 -7.2 -13.80 50.543 84.936 0.25 9.756 2.216 2.023 0.072 0.256 0.307 0.358 2.562 4.250 6.692 8890.042 0.0
2 2022-01-03 97.844 -9.4 8.8 -0.30 40.622 82.662 0.50 9.681 2.749 5.667 0.262 0.255 0.307 0.358 2.454 3.917 6.208 8924.833 0.0
3 2022-01-04 96.419 0.1 8.6 4.35 48.326 69.402 0.25 8.379 5.806 2.627 0.363 0.289 0.319 0.357 2.496 3.754 5.842 8838.292 0.0
4 2022-01-05 97.462 -11.1 -2.2 -6.65 50.341 76.828 0.00 5.717 4.207 1.251 0.126 0.313 0.337 0.357 1.688 3.429 5.567 8848.083 0.0
# Add day of the year column
df.insert(1, 'doy', df['datetime'].dt.dayofyear)
# Compute soil water storage in top 50 cm
df['storage'] = df['vwc_5cm']*100 + df['vwc_20cm']*200 + df['vwc_40cm']*200
# Plot timeseries of soil moisture and precipitation
plt.figure(figsize=(8,3))

plt.plot(df['datetime'], df['storage'], color='k', linewidth=1.0)
plt.ylabel('Soil water storage (mm)')

plt.twinx()

plt.plot(df['datetime'], df['prcp'], color='tomato', linewidth=0.5)
plt.ylabel('Precipitation (mm)', color='tomato')

plt.show()

# Find residual soil water storage
storage_min = df['storage'].min()
print(storage_min)

# Define model by forcing minimum storage
model = lambda t,tau,A: A * np.exp(-t/tau) + storage_min
90.80000000000001

Extract drydowns

The loop below scans the storage time series one day at a time:

  • Compute the change in storage relative to the previous day.
  • If storage decreased (or increased by less than the tolerance, which we treat as a negligible rainfall event), the drydown continues, so we add the day to the current event.
  • If storage increased by more than the tolerance, the drydown ends. Events shorter than drydown_min_length days are discarded; longer events are fitted with the exponential model and saved.
  • Reset the variables and start looking for the next drydown.
# Iterate over soil moisture timeseries to retrieve drydowns
day_counter = 0
drydown_min_length = 7 # days
tolerance = 1 # mm. Ignore storage increases smaller than this value (small rainfall events)
all_drydowns = []
drydown_event = {'date':[],'storage':[],'doy':[],
                 'days':[],'length':[], 'par':[]}

# We start the loop on the second day
for i in range(1,len(df)):
    delta = df["storage"][i] - df["storage"][i-1]
    
    if delta < tolerance:
        day_counter += 1
        drydown_event['date'].append(df.loc[i,'datetime'])
        drydown_event['storage'].append(df.loc[i,'storage'])
        drydown_event['doy'].append(df.loc[i,'doy'])
        drydown_event['days'].append(day_counter)
        drydown_event['length'] = day_counter

    else:
        # Avoid saving data for short drydowns
        if day_counter < drydown_min_length:
            
            # Reset variables
            day_counter = 0
            drydown_event = {'date':[],'storage':[],'doy':[],
                             'days':[],'length':[], 'par':[]}
            continue
        else:
            
            # Fit model to drydown event
            par_opt, par_cov = curve_fit(model, 
                                         drydown_event['days'], 
                                         drydown_event['storage'])
            drydown_event['par'] = par_opt
            
            # Append current event
            all_drydowns.append(drydown_event)
            
            # Reset variables
            day_counter = 0
            drydown_event = {'date':[],'storage':[],'doy':[],
                             'days':[],'length':[], 'par':[]}

        
print('There are a total of',len(all_drydowns),'drydowns')   
There are a total of 35 drydowns
# Inspect one drydown event
drydown = all_drydowns[8]
print('Start date:', drydown['date'][0].date())
print('Length:', drydown['length'], 'days')
print('Initial storage:', round(drydown['storage'][0], 1), 'mm')
print('Final storage:', round(drydown['storage'][-1], 1), 'mm')
print('Fitted parameters (tau, A):', np.round(drydown['par'], 2))
Start date: 2022-07-03
Length: 22 days
Initial storage: 184.7 mm
Final storage: 104.7 mm
Fitted parameters (tau, A): [ 11.07 107.7 ]

Overlay soil moisture timeseries and extracted drydowns

plt.figure(figsize=(8,3))
plt.plot(df['datetime'], df['storage'], color='k', linewidth=1.0)
for event in all_drydowns:
    plt.plot(event['date'], model(np.asarray(event['days']), *event['par']), '-r')

plt.ylabel('Soil water storage (mm)')
plt.show()