Choosing a Base Surface for Stockpile Volumes
Two competent people process the same flight over the same stockpile and return volumes that differ by six percent. Neither made a mistake. One fitted a plane through the toe of the pile; the other used the terrain model from a survey flown before the material arrived. On a pile sitting in a shallow depression those two surfaces differ by thirty centimetres under the middle, and thirty centimetres over two thousand square metres is six hundred cubic metres.
The base surface is the largest discretionary choice in a volume computation, and it is usually made implicitly by whichever tool is in front of the operator. This page makes it explicit: what each candidate assumes, when each is right, and how to stop the chosen one drifting between surveys.
Why the base is a modelling assumption, not a measurement
The upper surface is measured: the drone saw the top of the pile and the reconstruction recorded it. The base is, by definition, the surface under the material — which the drone never saw. Every base is therefore a statement about what would be there if the pile were removed, and the four common candidates encode four different statements.
A fitted plane says the ground beneath is flat and continues the slope of the surrounding hardstanding. On a yard that has been graded, this is close to true. On natural ground with a hollow, it is not.
A prior terrain model says the ground beneath is what was measured before the material arrived. This is the only candidate grounded in observation, and it is right whenever it exists and whenever the ground has not been disturbed since — which on an active site is a real qualification, because loaders scrape the base as they work.
A fixed level says the ground is a constant elevation the client has specified, usually a design formation level. This is not an estimate of reality at all; it is a contractual reference, and a volume against it answers “how much material is above the design level”, which is often exactly the question.
A picked toe triangulation says the ground follows a surface interpolated through points a person selected around the base of the pile. It is the most flexible and the least reproducible: two operators pick different points, and the result depends on the pick.
Figure 1 — Four bases, four volumes, one pile. The spread is larger than any tolerance a client would accept, and it is invisible in the output file.
Minimal reproducible solution
Make the base an explicit, named, stored decision. The function below produces any of the four from a single declaration, and — critically — it persists the chosen base so that the next survey uses the same one.
import json
from pathlib import Path
import numpy as np
import rasterio
def resolve_base(mode: str, *, site_config: str, transform, shape_hw,
toe_xyz=None, prior_path=None) -> tuple[np.ndarray, dict]:
"""Produce the base surface for a site, reusing a stored one where it exists.
The site config is the contract: once a base has been fixed for a site,
later surveys read it back rather than re-deriving it, which is what stops
a pile's volume moving when nothing has been delivered or removed.
"""
cfg_path = Path(site_config)
cfg = json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
if mode == "plane":
if "plane" in cfg:
a, b, c = cfg["plane"] # reuse the frozen fit
else:
if toe_xyz is None:
raise ValueError("no stored plane and no toe points supplied")
a, b, c = fit_toe_plane(*toe_xyz)
cfg["plane"] = [a, b, c]
cfg_path.write_text(json.dumps(cfg, indent=2))
surface = plane_surface(a, b, c, transform, shape_hw)
params = {"plane": [a, b, c], "frozen": "plane" in cfg}
elif mode == "prior":
if prior_path is None:
raise ValueError("prior mode needs a pre-existing terrain raster")
surface, _ = align_to_grid(prior_path, transform, shape_hw)
params = {"prior": prior_path}
elif mode == "fixed":
level = cfg["fixed_level_m"] # must be in the config
surface = np.full(shape_hw, level, dtype="float32")
params = {"fixed_level_m": level}
else:
raise ValueError(f"unknown base mode {mode!r}")
return surface, {"base_mode": mode, **params}
The frozen flag in the returned parameters is small and load-bearing. A volume computed against a freshly fitted plane and one computed against a frozen plane are not comparable, and a monitoring series that silently mixes them will show steps that look like material movement.
Note that picked-toe mode is deliberately absent. It can be supported — as a stored polygon of picked points, treated exactly like the frozen plane — but it should never be re-picked per survey, and making it awkward to do so is a feature.
Edge-case matrix
| Situation | Best base | Why |
|---|---|---|
| Graded yard, no prior survey | Fitted plane, frozen | Ground genuinely is near-planar |
| Pre-clearance survey exists | Prior terrain | The only measured option |
| Contract references a formation level | Fixed level | Answers the question actually asked |
| Pile on natural undulating ground | Prior terrain, else picked toe | A plane misses the hollow |
| Pile against a retaining wall | Prior terrain or fixed | A plane fit is unconstrained on one side |
| Pile growing outward each month | Frozen plane or prior | A re-fitted plane tilts as the toe moves |
| Base scraped by loaders since the prior survey | Fitted plane | The prior terrain no longer exists |
| Two piles sharing a toe | One base per pile, both frozen | A shared fit tilts toward the larger pile |
The scraped-base row is the one that catches monitoring contracts. A prior terrain model is the best base right up until a loader takes fifteen centimetres off the yard while working the pile, after which it is systematically wrong in a direction that inflates the volume. Re-surveying the base whenever the pile is fully cleared — and noting the date in the site config — is the cheap remedy.
Verification snippet
Because the four bases are cheap to evaluate, the strongest verification is to compute all of them and look at the spread. A small spread means the choice does not matter on this pile; a large one means it matters a great deal and the delivered figure needs its assumption stated prominently.
import numpy as np
def base_sensitivity(surface: np.ndarray, mask: np.ndarray, cell: float,
bases: dict[str, np.ndarray]) -> dict:
"""Volume under every candidate base, plus the spread between them."""
vols = {}
for name, base in bases.items():
dz = np.where(mask, surface - base, np.nan)
vols[name] = float(np.nansum(dz) * cell ** 2)
values = np.array(list(vols.values()))
spread = float((values.max() - values.min()) / abs(values.mean()))
verdict = ("base choice is immaterial here" if spread < 0.02
else "state the base assumption prominently in the deliverable")
return {"volumes_m3": vols, "relative_spread": spread, "verdict": verdict}
Running this once per site, at the start of a monitoring contract, is twenty seconds of compute that settles an argument before it happens. On a graded yard the spread is often under one percent and the whole question evaporates. On natural ground it is frequently five to eight percent, and knowing that before the first invoice is worth considerably more than the compute.
Figure 2 — Run the sensitivity once per site. It converts a general worry into a site-specific fact.
Figure 3 — The cost of a base that moves. The investigation this triggered cost more than the whole survey programme.
When to escalate
- The prior terrain model and the current one disagree on bare hardstanding. That is a datum or alignment problem, not a base-surface one, and differencing anything before fixing it will mislead. See aligning two epochs with ICP before differencing.
- The client’s own figure disagrees and both bases are defensible. The two numbers answer different questions. Put both in the report with their assumptions named rather than negotiating toward a single figure.
- The pile sits partly outside the survey extent. No base choice fixes missing data. Re-fly with a wider boundary; a volume from a truncated pile is not a volume.