Skip to content

Single-well and step tests

Many tests have no observation well: only the drawdown in the pumped well. That record carries well losses, skin and the water stored in the casing, and a single well cannot separate storativity from skin. The examples below use synthetic records, made with the solutions themselves, so the true values are known.

A synthetic single-well test

import numpy as np
from pumptest import Observation, PapadopulosCooper, PumpingTest, Schedule, Well

second = 1 / 86400
truth = {"T": 1800.0, "S": 4e-4, "Sk": 25.0, "rc": 0.09}
t = np.geomspace(5 * second, 750 * second, 25)
model = PapadopulosCooper(skin=True, casing_radius=True)
template = PumpingTest(Well(0.185), Schedule.constant(690.0),
                       [Observation("pumped well", 0.0, t, np.ones_like(t))])
s = model.drawdown(truth, template, template.observations[0])
s = s + np.random.default_rng(2).normal(0.0, 0.003, t.size)  # 3 mm of noise
test = PumpingTest(Well(0.185), Schedule.constant(690.0), [Observation("pumped well", 0.0, t, s)])

Jacob's straight line

from pumptest import cooper_jacob

cj = cooper_jacob(t, s, rate=690.0, distance=0.185, start=60 * second)
print(f"T = {cj.transmissivity:.0f} m2/d from a slope of {cj.line.slope:.4f} m per log cycle")

The straight line from 60 s gives 1657 m²/d, 8 percent below the 1800 m²/d the record was made with.

Large-diameter well with skin

The Papadopulos-Cooper solution includes the water stored in the casing. With skin=True it adds a skin factor; with casing_radius=True it fits the effective casing radius, because a pump column in the casing leaves less area for the water level to fall in. Hold S at an independent value:

from pumptest import fit

result = fit(test, model, fixed={"S": 4e-4})
print(result.summary())

The fit recovers T = 1822 m²/d, a skin of 25.4 and rc = 0.090 m. The correlation between T and the skin is 1.00: from a pumped well alone the two are not separately determined, and the standard errors understate that.

Dug wells

The same model is the standard one for large-diameter dug wells: give the well's radius as both radius and casing_radius, and fit with the observation at distance=0.

Step-drawdown test

Give the drawdown each step reached after the same time in the step, or the cumulative drawdown at the end of equal steps. Here the drawdowns follow s = BQ + CQ² with B = 3.2 × 10⁻³ d/m² and C = 1.5 × 10⁻⁷ d²/m⁵, plus 0.1 m of noise:

from pumptest import step_drawdown

rates = np.array([1300.0, 1700.0, 2400.0, 3300.0, 4100.0, 5000.0])
drawdowns = 3.2e-3 * rates + 1.5e-7 * rates**2
drawdowns = drawdowns + np.random.default_rng(3).normal(0.0, 0.1, rates.size)
steps = step_drawdown(rates, drawdowns)
print(f"B = {steps.aquifer_loss_coefficient:.3e} d/m2, C = {steps.well_loss_coefficient:.3e} d2/m5")
for q, e in zip(steps.rates, steps.efficiencies):
    print(f"  {q:6.0f} m3/d: well efficiency {e:.0%}")
print(f"drawdown at 6000 m3/d: {steps.drawdown(6000):.1f} m")

pumptest fits s = BQ + CQ² by least squares in drawdown, the quantity that was measured, rather than the usual regression of s/Q on Q (see decision 6).

Recovery

When pumping stops, the residual drawdown gives T again without the well losses. With a schedule that stops, every model fits the recovery directly:

from pumptest import Theis

truth = {"T": 300.0, "S": 1e-4}
t = np.linspace(0.02, 2.0, 60)
template = PumpingTest(Well(0.1), Schedule.constant(400.0, stop=1.0),
                       [Observation("OW", 20.0, t, np.ones_like(t))])
s = Theis().drawdown(truth, template, template.observations[0])
recovery_test = PumpingTest(template.well, template.schedule, [Observation("OW", 20.0, t, s)])
print(fit(recovery_test, Theis()).values)

For the hand method, theis_recovery(t, residual, rate, pumping_duration) fits the straight line of residual drawdown against t/t'.