Raster and Grid Assertions
Raster and grid assertions are the gridded-data counterpart to the vector patterns catalogued across Spatial Test Pattern Design & Implementation. The framework transfers — contract, validity, alignment, parity, performance — but every predicate inside it is different, and the differences are not cosmetic. A raster has no geometry to be invalid; it has a grid that can be misaligned, a mask that can be inverted, a dtype that can overflow, and a nodata value that can be silently counted as data. This page covers what each of those failure classes looks like, the assertions that catch them with rasterio 1.3+ and NumPy, and the one property that matters more than all the pixel values combined.
That property is alignment. Two rasters with identical pixel content and geotransforms differing by half a cell cannot be overlaid without resampling, and resampling changes values without raising anything. Almost every raster defect that reaches production is an alignment defect wearing a different name.
The Contract Comes Before the Pixels
A raster’s metadata is its contract, and it carries more risk than its data. Six properties determine whether any downstream operation is meaningful, and a fixture or a pipeline output that gets these right can have entirely arbitrary pixel values without weakening a single test.
The geotransform maps pixel coordinates to world coordinates. It is six numbers, and getting any of them wrong displaces or rescales the whole raster. The CRS says which world those coordinates belong to; a correct geotransform in the wrong CRS is a raster in the wrong place. The nodata value declares which pixels are fill rather than measurement, and it must be both declared and actually present in the array for masking to be exercised. The dtype decides what arithmetic does — an unsigned 8-bit band wraps rather than saturating, so a difference operation produces 250 instead of −6. Band count and order determine what index-based access reads. Block or tile size governs how windowed reads behave, and a fixture whose blocking differs from production tests a different access pattern than the one that runs.
The nodata row deserves the most attention because it is the one most often got half-right. Declaring a value without ever writing it into the array produces a fixture that never exercises masking; writing the value without declaring it produces one where the fill is treated as data and drags every statistic. A correct raster fixture does both, so a test can assert the mask’s shape and that statistics over the valid region ignore the fill.
Alignment Is the Assertion That Matters
Two rasters are aligned when a cell in one corresponds exactly to a cell in the other — same CRS, same pixel size, same origin offset modulo the cell size. When they are, an overlay is a cell-by-cell operation and every value is preserved. When they are not, every overlay silently resamples, and resampling changes values without producing an error.
The check is arithmetic rather than geometric. Given two geotransforms with pixel sizes
The practical consequence for a suite is that alignment must be asserted before any comparison of values, exactly as CRS identity must be asserted before any geometric comparison. Comparing two misaligned rasters cell by cell produces differences everywhere and a report nobody can read; asserting alignment first produces one failure that names the actual problem.
Assertion Reference
| Assertion family | What it checks | Tolerance form |
|---|---|---|
| Contract | dtype, band count, CRS, nodata declared | Exact |
| Geotransform | Six affine coefficients against the contract | Absolute, CRS units |
| Alignment | Pixel size equal, origin offset a whole cell | Absolute, sub-pixel |
| Mask consistency | Mask shape matches declared nodata placement | Exact, cell counts |
| Band statistics | Mean, min, max, valid-cell count over the mask | Relative epsilon |
| Value comparison | Cell-by-cell difference after alignment | Absolute, in band units |
| Resampled comparison | Statistics preserved within a stated bound | Relative epsilon |
The distinction between the last two rows is the one that keeps a raster suite honest. Two rasters that are aligned can be compared cell by cell with a tight absolute bound. Two rasters where one has been resampled cannot — resampling redistributes values, so the correct assertion is on aggregate properties (mean, total, histogram shape) with a bound derived from the resampling method rather than on individual cells.
Statistics Must Respect the Mask
Every summary statistic over a raster is wrong unless it excludes nodata, and the failure is quiet: a fill value of −9999 in a small fraction of cells drags a mean far below any plausible measurement, while a fill value of 0 drags it towards zero in a way that looks merely surprising rather than obviously broken.
Asserting statistics therefore has two parts that must both be present. First, the valid-cell count — how many cells contributed — because a statistic computed over an unexpected number of cells is meaningless even if its value looks reasonable. Second, the statistic itself, computed over the masked array rather than the raw one. A suite that asserts the second without the first cannot distinguish a correct mean from a correct-looking mean computed over half the raster.
Where These Checks Run
Contract and alignment assertions are cheap — they read metadata rather than pixels — and belong in the fast pre-merge tier alongside their vector equivalents. Statistics over a small fixture are equally cheap. What is expensive is anything that reads a full production-sized raster, and that belongs in the scheduled tier, with the pre-merge lane running against a deliberately small grid whose blocking matches production.
The fixture-size decision is worth making explicitly: a 32 by 32 grid exercises alignment, masking, dtype behaviour and statistics exactly as well as a 4096 by 4096 one, and reads in microseconds. Larger fixtures are needed only for tests about block or overview behaviour, where the array must span several blocks for the access pattern to exist at all. The mechanics of building either belong to raster mocking techniques.
Comparing Two Rasters When Exact Equality Is Impossible
Once a raster has been resampled, reprojected, or written through a lossy compression, a cell-by-cell comparison is the wrong instrument. Resampling redistributes values by design — that is what it is for — so an exact comparison reports differences everywhere and none of them are defects. The correct assertions move up a level, to properties that a faithful transformation preserves and a broken one does not.
Total or mean over the valid region. A resampling that conserves the quantity should leave the sum close to unchanged for an extensive variable and the mean close to unchanged for an intensive one. Which of the two applies is a property of the data, not of the raster, and getting it wrong is a common source of assertions that fail for correct behaviour.
Histogram shape. Nearest-neighbour resampling preserves the set of values exactly and only changes their counts; bilinear and cubic introduce intermediate values by construction. Asserting that a categorical raster’s value set is unchanged after resampling is therefore a precise check — and it is also how a nearest-neighbour requirement is enforced, since any interpolating method breaks it immediately.
Extent and cell count. The resampled raster should cover the same ground within a fraction of a cell, and its dimensions should follow arithmetically from the new pixel size. A cell count that is off by one is usually a rounding convention rather than a defect, and asserting the expected value makes the convention explicit rather than incidental.
Per-cell difference at a chosen quantile. Where a cell-level bound is genuinely needed, bound the 95th or 99th percentile of the absolute difference rather than the maximum. The maximum after resampling is dominated by edge cells and cells adjacent to nodata, where interpolation has the least information, and bounding it forces a threshold so loose it stops discriminating anywhere else.
| Resampling method | Preserves the value set | Right assertion |
|---|---|---|
| Nearest neighbour | Yes, exactly | Value set unchanged; counts may shift |
| Bilinear | No — introduces intermediates | Mean within a relative epsilon |
| Cubic | No — may overshoot the input range | Mean, plus a range bound catching overshoot |
| Average | No | Sum preserved for extensive quantities |
| Mode | Yes, for categorical data | Value set unchanged; dominant class preserved |
The cubic row carries a trap worth naming. Cubic interpolation can produce values outside the range of its inputs — an overshoot at a sharp edge — which for a physically bounded quantity such as a reflectance or a percentage produces impossible values that no statistical assertion notices. Adding an explicit range bound after any cubic resampling costs one line and catches a class that means the data is unusable.
Testing the Read Path, Not Only the Data
A raster test can pass entirely in memory and still miss the defects that matter in production, because a large share of raster behaviour lives in how the file is read rather than in what it contains. Three properties belong to the read path and need their own assertions.
Windowed reads. Production code reading a subregion should transfer only the blocks intersecting that window. A test that reads the whole array and slices it in NumPy exercises none of that, and a regression that quietly turns a windowed read into a full read is invisible to every value assertion. Asserting on what was read — not just what was returned — requires a fixture whose blocking is real.
Overview selection. When a reader asks for a coarse view, it should read a reduced-resolution level rather than downsampling the full-resolution data. Both produce the same pixels, so only an assertion about which level was accessed can tell them apart, and the difference is often an order of magnitude in bytes moved.
Masked reads. Reading with masking enabled returns a masked array whose fill is excluded from arithmetic; reading without it returns raw values including the sentinel. Code that assumes one and receives the other produces statistics that are wrong in the quiet way described earlier, and the assertion that catches it is on the returned type rather than on any value.
Each of these needs a fixture that is a real file with real structure rather than an array with a profile dictionary attached — which is the practical argument for building fixtures through an in-memory file rather than as bare arrays.
Frequently Asked Questions
How small can a raster fixture be and still be useful?
Small enough to read instantly, which for most assertions means tens of cells rather than thousands. Alignment, masking, dtype behaviour and statistics are all exercised identically by a 32 by 32 grid and a 4096 by 4096 one. The exception is anything about blocking or overviews, which needs an array spanning several blocks for the behaviour to exist at all — and those fixtures should be documented as deliberately large so nobody optimises them away.
Should a raster suite assert on pixel values at all?
Sparingly, and never as the primary check. Values are the least stable property of a raster — they change under any resampling, compression or dtype conversion — while the metadata contract and the alignment are exactly the properties that must not change. A suite built on value assertions is brittle and passes over the defects that matter; one built on contract and alignment catches those and can add value checks where they genuinely apply.
How do you test a raster that is produced by an external service?
Assert the contract on arrival and treat the values as data rather than as code output. The service’s correctness is not yours to test, but its conformance to what your pipeline requires is: dtype, CRS, nodata, geotransform, extent, and band count. When those hold, downstream failures are attributable; when they are not asserted, every downstream failure begins with a question about the input.
What is the raster equivalent of a topology check?
Continuity across tile edges. A dataset delivered as tiles can be individually valid and collectively wrong — a seam where adjacent tiles disagree by more than the data’s own variation, or a gap where a tile is missing entirely. The check is a set-level one, exactly like gap detection in a vector coverage, and it belongs in the same scheduled tier for the same reason.
Do compression settings need testing?
Only where they can change values. A lossless codec is a storage decision and needs no assertion beyond the file being readable. A lossy one changes pixel values by design, which turns every downstream comparison into a tolerance question, and the tolerance must then be derived from the codec’s settings rather than guessed. Asserting which codec was used is the cheap way to prevent a lossy setting arriving unnoticed.
Common Failure Modes and Gotchas
- Comparing values before asserting alignment. Two misaligned rasters differ in every cell, producing a report that says nothing. Alignment is the raster equivalent of CRS identity: assert it first, and fail on it alone.
- Unsigned dtype arithmetic. Subtracting two
uint8bands wraps rather than going negative, so a difference of −6 reads as 250. Cast to a signed or floating type before any difference operation, and assert the dtype rather than trusting it. - A declared nodata that never appears. The mask is empty, masking is never exercised, and the first real fill value in production behaves untested. Put the value in the array deliberately.
- Asserting a statistic without its cell count. A plausible mean computed over the wrong population passes indefinitely. The count is the cheap half of the assertion and the half that discriminates.
- Fixtures whose blocking differs from production. A striped fixture cannot exercise the tiled access pattern the production reader uses, so windowed-read behaviour goes untested however thorough the value assertions are.
- Treating a resampled comparison as an exact one. Resampling redistributes values by design; comparing cell by cell after it produces failures that are correct behaviour. Compare aggregates, with a bound derived from the method.
Conclusion
Raster assertions are not vector assertions with different data. The metadata carries the risk, alignment is the property that decides whether any comparison is meaningful, masks decide whether any statistic is, and the dtype decides whether arithmetic means what it appears to. A suite that asserts contract, then alignment, then masked statistics, then values — in that order, against a small fixture whose blocking matches production — covers the gridded half of spatial test pattern design with the same rigour the vector patterns bring to geometry.
Related
- Spatial Test Pattern Design & Implementation — the parent catalogue and the framework these assertions instantiate.
- Asserting Raster Band Statistics in pytest — the runnable statistics gate and its cell-count half.
- Testing Nodata and Mask Handling in rasterio — mask construction, inversion and the fill that is never exercised.
- Comparing Resampled Rasters with Tolerance — what to assert once an exact comparison is impossible.
- Raster Mocking Techniques — building the fixtures these assertions run against.
- Coordinate Reference System Testing — the CRS half of the alignment contract.