soil_heat.wang_and_bouzeid
Python utilities implementing the equations from:
Wang, Z.-H., & Bou-Zeid, E. (2012). A novel approach for the estimation of soil ground heat flux. Agricultural and Forest Meteorology, 154-155, 214-221.
All equations appearing in that paper are reproduced as vectorised Python functions, together with a few convenience helpers. The library is fully NumPy-aware and can be used on scalars or on time-series arrays.
Units
All functions assume SI units throughout:
depth
z― m (positive downward)time
t― stemperature
T― K (or °C provided the 0-offset is handled consistently)heat flux
G, H, LE― W m-2radiative flux
Rn― W m-2- soil properties
thermal conductivity
k― W m-1 K-1heat capacity
rho_c― J m-3 K-1thermal diffusivity
kappa = k / rho_c― m² s-1
Dependencies
>>> pip install numpy scipy
Example
>>> import numpy as np, wang_and_bouzeid as sghf
>>> # 30-minute series (dt = 1800 s) of flux-plate measurements at z = 0.08 m
>>> Gz = np.loadtxt('Gz_8cm.txt')
>>> G0 = sghf.estimate_G0_from_Gz(Gz, z_r=0.08, kappa=0.7e-6, dt=1800)
Attributes
Functions
|
Compute the closure residual of the surface energy balance (SEB). |
|
Conventional estimate of surface ground heat flux, Eq. (2). |
|
Green-function solution \(g_z(t)\) for the **one‐dimensional, |
|
Temperature time-series at depth z via Duhamel convolution (Eq. 6). |
|
Compute G(z,t) from a known surface flux series G0 (Eq. 9). |
|
Estimate surface ground heat flux G0 from plate measurements Gz. |
|
Evaluate a sinusoidal surface heat-flux forcing |
|
Analytical solution for soil temperature beneath a sinusoidally |
|
Analytical soil heat–flux response G(z,t) to a sinusoidal |
|
Volumetric heat capacity of moist soil, Eq. (16). |
|
Return Pf (Eq. 18) from volumetric water content. |
|
Thermal conductivity parameterisation, Eq. (17). |
|
Return κ = k / (ρ c). |
Module Contents
- soil_heat.wang_and_bouzeid.energy_balance_residual(Rn: float | numpy.ndarray, H: float | numpy.ndarray, LE: float | numpy.ndarray, G0: float | numpy.ndarray) float | numpy.ndarray[source]
Compute the closure residual of the surface energy balance (SEB).
The classical SEB for land–atmosphere exchange is
\[R_n - G_0 \;=\; H + LE +\]arepsilon ,
where the residual term :math:`
- arepsilon` quantifies the lack of
closure. Rearranging gives
\[\]
arepsilon ;=; R_n - G_0 - H - LE ,
which is what this helper returns.
- Rnfloat or array_like
Net radiation Rₙ (W m⁻²). Positive downward.
- Hfloat or array_like
Sensible heat flux H (W m⁻²). Positive upward (atmosphere ← surface).
- LEfloat or array_like
Latent heat flux LE (W m⁻²). Positive upward.
- G0float or array_like
Ground (soil) heat flux G₀ (W m⁻²). Positive downward (into the soil). Some authors use the opposite sign convention; ensure consistency with Rn.
- float or ndarray
Energy‐balance residual :math:`
- arepsilon` (W m⁻²) with the
broadcast shape of the inputs. Positive values indicate missing energy (surface gains > turbulent + ground fluxes), whereas negative values mean an apparent energy surplus.
Broadcasting – All inputs are treated with NumPy broadcasting, allowing scalars, 1-D arrays, or DataFrame columns.
Closure diagnostics – The residual can be summarised as a mean bias or expressed as a relative closure fraction
1 − ε / (Rn − G0).Typical magnitudes – Eddy‐covariance towers often report 10–30 % non-closure. Persistently large |ε| values may indicate advective transport, sensor tilt, or data‐processing issues.
>>> ε = energy_balance_residual( ... Rn=df["Rn"], H=df["H"], LE=df["LE"], G0=df["G"] ... ) >>> ε.describe(percentiles=[0.05, 0.5, 0.95]) count 17280.000000 mean -9.73 std 34.51 5% -58.42 50% -8.11 95% 39.12 Name: residual, dtype: float64
Plot daily average residuals:
>>> ε.resample("D").mean().plot(marker="o") >>> plt.axhline(0, color="k", lw=0.8) >>> plt.ylabel("Energy balance residual (W m$^{-2}$)") >>> plt.title("Daily SEB closure")
- soil_heat.wang_and_bouzeid.ground_heat_flux_conventional(k: float, dT_dz_at_zr: float, rho_c: float, dT_dt_profile: Sequence[float], z_profile: Sequence[float]) float[source]
Conventional estimate of surface ground heat flux, Eq. (2).
- Parameters:
k – Effective soil thermal conductivity (W m-1 K-1).
dT_dz_at_zr – Vertical temperature gradient evaluated at the flux-plate depth
z_r(K m-1). A negative gradient means temperature decreases with depth.rho_c – Volumetric heat capacity of the soil (J m-3 K-1).
dT_dt_profile – Time derivatives ∂T/∂t for each node between the surface and
z_r(K s-1). Any iterable (list, ndarray, …). Must align withz_profile.z_profile – Depth of each node in the temperature profile (m). Increasing, positive downward, excluding the surface (z = 0) but including
z_r(last element).
- Returns:
G0 – Ground heat flux at the surface (W m-2). Positive into the soil.
- Return type:
- soil_heat.wang_and_bouzeid.green_function_temperature(z: float, t: float, kappa: float) float[source]
Green-function solution \(g_z(t)\) for the one‐dimensional, semi-infinite heat equation with a unit surface‐flux impulse at \(t=0,\,z=0\).
The governing partial differential equation is
\[\frac{\partial T}{\partial t} \;=\; \kappa\,\frac{\partial^{2} T}{\partial z^{2}}, \qquad z \ge 0,\; t > 0,\]with initial condition \(T(z,0)=0\) and boundary condition corresponding to a Dirac δ–heat pulse applied at the surface (\(z=0\)). The resulting Green function (Carslaw & Jaeger, 1959, Eq. 7) is
\[g_z(t) \;=\; \frac{2}{\sqrt{\pi}} \,\sqrt{\kappa t}\; \exp\!\Bigl[-\frac{z^{2}}{4\kappa t}\Bigr] \;-\; z\,\operatorname{erfc}\! \Bigl[\frac{z}{2\sqrt{\kappa t}}\Bigr], \qquad t>0,\]and \(g_z(t)=0\) for \(t\le 0\) (causality).
- Parameters:
- Returns:
Green-function value \(g_z(t)\) (units m, because the solution integrates heat-flux density with respect to depth to yield temperature).
- Return type:
Notes
Causality check – If
t≤ 0 the function short-circuits and returns 0.0.Vectorisation – For vector or array input use
numpy.vectorize()or wrap the function innumpy.frompyfunc(); the core implementation is scalar for numerical clarity.Usage –
g_z(t)can be convolved with an arbitrary surface heat-flux time seriesq₀(t)to obtain temperature at depth via\[T(z,t) \;=\; \int_{0}^{t} g_z(t-τ)\,q_0(τ)\;\mathrm dτ .\]
References
Carslaw, H. S., & Jaeger, J. C. (1959). Conduction of Heat in Solids (2nd ed., p. 100). Oxford University Press.
Examples
>>> g = green_function_temperature(z=0.05, t=3_600, kappa=1.4e-7) >>> print(f"g(5 cm, 1 h) = {g:.3e} m") g(5 cm, 1 h) = 7.42e-04 m
- soil_heat.wang_and_bouzeid.temperature_convolution_solution(z: float, t_series: numpy.ndarray, f_series: numpy.ndarray, kappa: float, Ti: float = 0.0) numpy.ndarray[source]
Temperature time-series at depth z via Duhamel convolution (Eq. 6).
T(z,t) = Ti + ∫ f(t-τ) d g_z(τ)The integral becomes a discrete convolution where f is the boundary heat-flux series (W m-2 → ∂T/∂z via Fourier).
- soil_heat.wang_and_bouzeid.soil_heat_flux_from_G0(z: float, t_series: numpy.ndarray, G0_series: numpy.ndarray, kappa: float) numpy.ndarray[source]
Compute G(z,t) from a known surface flux series G0 (Eq. 9).
- soil_heat.wang_and_bouzeid.estimate_G0_from_Gz(Gz_series: numpy.ndarray, z_r: float, kappa: float, dt: float) numpy.ndarray[source]
Estimate surface ground heat flux G0 from plate measurements Gz.
Implements discretised Eq. (11) – the recursion proposed by Wang & Bou-Zeid (2012). Time-series must be regularly sampled.
- soil_heat.wang_and_bouzeid.sinusoidal_boundary_flux(t: float | numpy.ndarray, A: float, omega: float, epsilon: float) float | numpy.ndarray[source]
Evaluate a sinusoidal surface heat-flux forcing
\[q_0(t) \;=\; A \,\sin\!igl(\omega t +\]arepsilonigr),
which is commonly used as a boundary condition for analytical soil- heat-flux solutions (see Eq. 13 of the companion text).
- tfloat or array_like
Time since the start of the simulation (s). May be scalar or any NumPy-broadcastable shape; units must be consistent with
omega.- Afloat
Amplitude of the surface heat flux (W m⁻²). Positive downward into the soil.
- omegafloat
Angular frequency (rad s⁻¹). For a diurnal cycle
omega = 2 π / 86 400≈ 7.272 × 10⁻⁵ rad s⁻¹.- epsilonfloat
Phase shift ε (rad). Positive values delay the flux peak, negative values advance it.
- ndarray or float
Instantaneous surface heat flux q₀(t) (W m⁻²) with shape given by NumPy broadcasting of the inputs.
Sign convention — Positive q₀ adds energy to the soil column; be sure it matches the sign convention of your governing equation.
Vectorisation — The implementation is a single call to
numpy.sinand therefore fully vectorised.Period — The period P (s) of the forcing is related to
omegaby P = 2 π / ω.
>>> import numpy as np, matplotlib.pyplot as plt >>> t = np.linspace(0, 2*86400, 1_000) # 2 days >>> q0 = sinusoidal_boundary_flux( ... t, A=120, omega=2*np.pi/86400, epsilon=0) >>> plt.plot(t/3600, q0) >>> plt.xlabel("Time (h)") >>> plt.ylabel("Surface heat flux $q_0$ (W m$^{-2}$)") >>> plt.title("Sinusoidal surface forcing (A = 120 W m⁻²)") >>> plt.show()
- soil_heat.wang_and_bouzeid.soil_temperature_sinusoidal(z: float, t: float | numpy.ndarray, A: float, omega: float, epsilon: float, Ti: float, kappa: float) float | numpy.ndarray[source]
Analytical solution for soil temperature beneath a sinusoidally forced surface heat‐flux boundary.
The temperature response of a semi-infinite, homogeneous soil column to a surface heat flux
\[q_0(t) \;=\; A \sin(\omega t +\]arepsilon)
is (Carslaw & Jaeger 1959, Eq. 14)
\[T(z,t) \;=\; T_i \;+\; \underbrace{\frac{A}{\kappa\sqrt{\omega}} e^{-z r}\, \sin\!igl(\omega t +\]- arepsilon - z r - π/4bigr)}
_{text{steady harmonic}} ;+; underbrace{T_{text{trans}}(z,t)} _{text{transient integral}} ,
where \(r = \sqrt{\omega / 2\kappa}\). The first term is the steady periodic component that propagates downward with exponentially damped amplitude and a depth-dependent phase lag. The second term accounts for the transient adjustment from the initial uniform temperature Tᵢ and is evaluated numerically here by vectorised trapezoidal quadrature.
- zfloat
Depth below the soil surface (m, positive downward).
- tfloat or array_like
Time since the start of the forcing (s). Scalar or NumPy array.
- Afloat
Amplitude of the sinusoidal surface heat flux (W m⁻², positive downward). Consistent with
sinusoidal_boundary_flux().- omegafloat
Angular frequency of the forcing (rad s⁻¹). For a diurnal wave
omega = 2 * π / 86_400.- epsilonfloat
Phase shift ε (rad) of the surface heat-flux wave.
- Tifloat
Initial uniform soil temperature Tᵢ (°C or K).
- kappafloat
Thermal diffusivity κ of the soil (m² s⁻¹).
- float or ndarray
Soil temperature T(z, t) in the same units as Ti. If t is an array the returned array has the same shape.
Steady component – The exponential term
exp(-z*r)dampens amplitude with depth while the phase lag isz*r + π/4.Transient component – The integral is truncated at
x = 50 / z(or 50 if z = 0) and evaluated with 2 000 panels, which empirically yields < 0.1 % relative error for typical soil parameters. Adjust the limits or panel count for higher precision.Vectorisation – For array t the quadrature is performed in parallel using broadcasting; memory usage scales with
len(t)× 2 000.Units – Ensure A is W m⁻² heat flux. If you have a surface temperature wave instead, transform to an equivalent heat-flux boundary or modify the formulation.
>>> import numpy as np, matplotlib.pyplot as plt >>> z = 0.05 # 5 cm >>> k = 1.4e-7 # m² s⁻¹ >>> A = 120 # W m⁻² >>> ω = 2*np.pi/86400 # diurnal >>> ε = 0 # no phase shift >>> Ti = 15.0 # °C >>> t = np.linspace(0, 172800, 2000) # 2 days >>> Tz = soil_temperature_sinusoidal(z, t, A, ω, ε, Ti, k) >>> plt.plot(t/3600, Tz) >>> plt.xlabel("Time (h)") >>> plt.ylabel("Temperature (°C)") >>> plt.title("Analytical diurnal soil temperature at 5 cm") >>> plt.show()
- soil_heat.wang_and_bouzeid.soil_heat_flux_sinusoidal(z: float, t: float | numpy.ndarray, A: float, omega: float, epsilon: float, kappa: float) float | numpy.ndarray[source]
Analytical soil heat–flux response G(z,t) to a sinusoidal surface–flux boundary condition.
A semi-infinite, homogeneous soil column forced at the surface by
\[q_0(t) \;=\; A \,\sin\!igl(\omega t +\]arepsilonigr)
admits the Green-function solution (Carslaw & Jaeger, 1959, Eq. 15)
\[G(z,t) \;=\; A\,e^{-z r}\,\sin(\omega t +\]- arepsilon - z r)
;+; G_{ ext{trans}}(z,t),
where \(r = \sqrt{ \omega / 2\kappa }\) and the transient term
\[G_{ ext{trans}}(z,t) \;=\; -\]- rac{2 A kappa}{pi}
int_{0}^{infty} frac{( kappa x^{2}sin
arepsilon - omega cos arepsilon )
;x sin(x z)}
{omega^{2} + kappa^{2} x^{4}}
,e^{-kappa x^{2} t}; mathrm d x .
The first term is the steady, exponentially damped harmonic that propagates downward with a depth-dependent phase lag; the second term describes the transient adjustment from an initially unheated half-space. The integral is evaluated here by vectorised trapezoidal quadrature on a finite domain (x ≤ 50 / z).
- zfloat
Depth below the soil surface (m, positive downward).
- tfloat or array_like
Time since the onset of forcing (s). Scalar or NumPy array.
- Afloat
Amplitude of the surface heat flux (W m⁻², positive downward).
- omegafloat
Angular frequency ω of the forcing (rad s⁻¹). For a diurnal wave
omega = 2 π / 86 400.- epsilonfloat
Phase shift ε of the surface flux (radians).
- kappafloat
Thermal diffusivity κ of the soil (m² s⁻¹).
- float or ndarray
Heat flux G(z,t) at depth z (W m⁻²) with shape equal to
tafter NumPy broadcasting. Positive values downward, matching the sign convention of A.
Steady component – Amplitude decays as
exp(-z*r); phase lag increases linearly with depth by z r.Transient component – The integration limit
50 / z(or 50 for z = 0) yields < 0.1 % relative error for common parameter ranges. Increase the upper bound and/or panel count (2 000) for stricter accuracy.Vectorisation – For array t the quadrature is evaluated for all time steps simultaneously via broadcasting; memory usage scales with
len(t) × 2000.Coupling with temperature – The companion function
soil_temperature_sinusoidal()gives T(z,t) under the same boundary forcing; G and T satisfy Fourier’s lawG = -k ∂T/∂zonce thermal conductivity k is specified.
Carslaw, H. S., & Jaeger, J. C. (1959). Conduction of Heat in Solids (2nd ed., pp. 100–102). Oxford University Press.
>>> import numpy as np, matplotlib.pyplot as plt >>> z = 0.05 # 5 cm >>> kappa = 1.4e-7 # m² s⁻¹ >>> A = 120 # W m⁻² downward >>> omega = 2*np.pi/86400 # diurnal frequency >>> eps = 0 >>> t = np.linspace(0, 172800, 2000) # 2 days >>> G = soil_heat_flux_sinusoidal(z, t, A, omega, eps, kappa) >>> plt.plot(t/3600, G) >>> plt.xlabel("Time (h)") >>> plt.ylabel("Heat flux $G$ (W m$^{-2}$)") >>> plt.title("Analytical diurnal soil heat flux at 5 cm") >>> plt.show()
- soil_heat.wang_and_bouzeid.heat_capacity_moist_soil(theta_v: float | numpy.ndarray, theta_s: float, rho_c_s: float = 1260000.0, rho_c_w: float = 4200000.0) float | numpy.ndarray[source]
Volumetric heat capacity of moist soil, Eq. (16).
- Parameters:
theta_v – Volumetric water content (m³ m-3).
theta_s – Porosity (saturated volumetric water content) (m³ m-3).
rho_c_s – Heat capacity of dry soil / water (J m-3 K-1).
rho_c_w – Heat capacity of dry soil / water (J m-3 K-1).
- soil_heat.wang_and_bouzeid.pf_from_theta(theta_v: float | numpy.ndarray, theta_s: float, psi_s: float, b: float) float | numpy.ndarray[source]
Return Pf (Eq. 18) from volumetric water content.