Skip to content

Architecture decisions

Why this system is the way it is. One record per decision that shapes the architecture or a delivery boundary, in the order they were taken.

Records are immutable once accepted. A decision that changes course gets its own record and supersedes the earlier one by reference, rather than the earlier one being edited to agree with the present.

This page is generated from pm/decisions/. Edit a record there, then run python scripts/check_decisions.py --write.

1. Record architecture decisions

Date: 2026-10-14

Status: accepted

Context

A new person, or the same team months later, needs to know not just what the architecture is but why it is that way. Decisions made in chat, in a proposal or in a pull request comment are lost to the repository.

Decision

Architecture decision records live in pm/decisions/, one short file per decision, numbered in sequence. Each states the context, the decision and its consequences. A record is written when a decision shapes the architecture or a delivery boundary.

The index at pm/decisions/README.md is generated, not written:

python scripts/check_decisions.py --write

Consequences

  • The reasoning behind the codebase is discoverable in the repository itself.
  • Records are immutable once accepted. A decision that changes course gets its own record and supersedes the earlier one by reference, rather than the earlier one being edited to agree with the present.
  • The format stays short, so writing one is never a barrier.
  • The decisions gate fails the build when the numbering, the required sections or the index drift.

2. A standalone open-source engine, numpy and scipy only

Date: 2026-10-14

Status: accepted

Context

PumpPilot, the pumping test product, needs an analysis engine. The commercial tools (AQTESOLV, AquiferTest, MLU) are closed. The open ones are either a MATLAB toolbox no longer maintained (Hytool), a Fortran program (USGS WTAQ), or general transient-flow libraries (TTim) whose interface is built around layered models rather than around a pumping test. The plan is to release the engine on its own first and build the product on it later.

Decision

pumptest is a separate repository and package under the MIT licence. Its runtime dependencies are numpy and scipy and nothing else. It does no input or output beyond what a caller hands it and never opens a network connection, so the egress gate is not installed.

Both dependencies are available in Pyodide, which is the intended route for running the same code in a browser later. Nothing runs it in a browser yet.

Consequences

  • Anyone can use, audit and cite the engine, which is the point of releasing it.
  • The product cannot rely on anything here that is not in the open release.
  • Reports, plotting and file formats belong to the product or to optional extras, not to the core package.

3. Consistent units, never converted inside the solvers

Date: 2026-10-14

Status: accepted

Context

Field data arrives in mixed units: rates in litres per minute or gallons per minute, time in minutes, levels in feet or metres below ground. A solver that converts silently has to guess which unit each number is in, and a wrong guess is a result that is wrong by a constant factor and still looks plausible.

Decision

Every solver takes and returns numbers in one consistent set chosen by the caller (metres, days and cubic metres per day is the usual one). Conversion is an explicit, separate step through pumptest.units.

Consequences

  • Transmissivity comes back in the caller's length squared per time unit.
  • The examples and tests state their units at the point of use.
  • A unit mistake shows up at the conversion call, where the unit names are written down, rather than inside a fit.

4. Solutions in the Laplace domain, inverted by Stehfest

Date: 2026-10-14

Status: accepted

Context

Only Theis has a convenient closed form in time. Leakage, wellbore storage, skin and the delayed water-table response all have simple closed forms in the Laplace domain. WTAQ and AQTESOLV invert those numerically.

Decision

Theis is evaluated directly from the exponential integral. Every other model is a Laplace-domain kernel inverted by the Stehfest algorithm with 16 terms, which holds Theis to 4e-5 relative wherever u <= 2. Confined, leaky, wellbore-storage and skin cases share one radial kernel. The Neuman kernel sums its vertical eigenfunction series to at most 400 terms.

The fitting Jacobian uses a relative finite-difference step of 1e-4, because Stehfest round-off near 1e-7 relative swamps scipy's default step and stops the optimiser on its first iteration (found while validating Texas Hill).

Consequences

  • A new solution is a new kernel, tested against its published table.
  • Stehfest is unsuited to oscillating responses; none of these solutions oscillate.
  • Relative accuracy is poor where drawdown is negligibly small (u above about 5), which does not affect fits because those drawdowns are tiny in absolute terms.

5. Validation against published fits and published tables

Date: 2026-10-14

Status: accepted

Context

An engine that claims to replace AQTESOLV has to show it gets the same answers. The TTim documentation (MIT) publishes real tests with the parameters AQTESOLV, MLU and Kruseman and de Ridder obtained. Kruseman and de Ridder (1994) tabulate the well functions themselves.

Decision

The suite carries two kinds of answer key, both as committed tests:

  1. Five published tests (Oude Korendijk, Gridley, Sioux Flats, Texas Hill, Dalem). Fitted parameters must be close to the published ones, and our fit must be at least as good as the published parameters evaluated on the same data. Printed RMSE values are not compared digit for digit, because they were computed on each program's own copy of the data.
  2. Values transcribed from the Kruseman and de Ridder annexes for Theis, Hantush, Neuman and Papadopulos-Cooper, within 0.6 percent (the tables carry three significant figures).

The unconfined and wellbore-storage fits are tested end to end on synthetic data, because the published comparisons for those cases use partial penetration, which is not modelled yet.

Consequences

  • "Matches AQTESOLV" is a test result, not a claim.
  • One table cell (Annex 4.2, u = 2e-3, r/L = 0.08) is a misprint in the book and is recorded as such in the test.

6. Step-drawdown coefficients by least squares in drawdown

Date: 2026-10-14

Status: accepted

Context

The textbook analysis of a step-drawdown test (Hantush-Bierschenk) draws a straight line through s/Q against Q. Fitting that line by least squares weights each step by 1/Q, so the low-rate steps dominate. On Clark's "Well 1" (Kruseman and de Ridder, Table 14.2) the s/Q regression missed the measured drawdowns by 0.119 m RMSE, worse than the book's hand-drawn line at 0.105 m.

Decision

step_drawdown fits s = B Q + C Q**2 by linear least squares on drawdown, the quantity that was measured. On "Well 1" this misses by 0.096 m, better than both.

Consequences

  • B and C differ slightly from a textbook s/Q regression on the same data; the docstring and the validation page say why.
  • The validation test asserts the drawdown fit beats the published line, not that it reproduces it.

7. Neuman roots by offset; delayed-yield starts from early and late Theis fits

Date: 2026-10-14

Status: accepted

Context

Fitting Neuman to Texas Hill took about three minutes, 95 percent of it in the eigenvalue solver, which iterated on the root itself from a rough guess and fell back to bisection. Replacing it with a fast solver exposed a second fault: every Neuman starting point began with S near 1e-7, and on Vennebulten all of them converged to a Theis-like answer (RMSE 0.0101 m instead of 0.0030 m). The slow solver had only been escaping that by accident.

Decision

  • Solve each root as n pi + d, Newton on the offset d from two fixed-point steps, which converges in a few iterations at full precision.
  • Start the delayed-yield fit from Theis fits to the first 40 and last 40 percent of each record: T and the elastic S from early time, Sy from late time, the hand method of matching type-A and type-B curves.
  • With more than three candidate starts, optimise only from the three with the lowest initial residual.

Consequences

  • Vennebulten fits in under 2 s and reaches the Neuman minimum.
  • A Neuman fit pushed onto leaky data (Kd near its lower bound) still needs the full 400-term series and takes about 16 s. Accelerating the series tail is the next step for speed; nothing does it yet.

8. Verify against papers and published program results, not textbook examples

Date: 2026-10-14

Status: accepted

Supersedes the textbook part of record 5.

Context

Record 5 compared the package with AQTESOLV results and with the worked examples and type-curve annexes of Kruseman and de Ridder (1994). A review of those comparisons found one parameter set attributed to the textbook (Dalem: T 1677 m2/d, c 331 d) that the book does not contain; the book's own three analyses give 1665 to 1883 m2/d and 216 to 578 d. The textbook answers are also graphical, so they bound a fit only loosely, and transcribed tables raise a permission question for redistribution.

Decision

  • Field comparisons use only data and parameters published in papers or by the programs that produced them: the AQTESOLV results reported by Yang (2020) and the welltestpy results of Mueller et al. (2022) on the open Lauswiesen data.
  • The Oude Korendijk, Dalem, Vennebulten, Hoogezand and step-drawdown "Well 1" records are removed from the package, the tests and the documentation.
  • Tutorials that need a test type no bundled record covers use synthetic records made with the model itself, and say so.
  • Well functions are checked against independent evaluations of the formulas in the original papers; the textbook annex tables are replaced in a following change.

Consequences

  • The Neuman and Papadopulos-Cooper models have no published field test until one from a paper is found; synthetic recovery tests cover their fitting.
  • The validation page and figures show seven tests: three AQTESOLV, four Lauswiesen.