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.

Six metadata properties and the assertion for each Six rows pairing a raster metadata property with its failure consequence and the assertion that detects it. A wrong geotransform displaces or rescales every overlay, detected by comparing the six affine coefficients against the declared contract. A wrong coordinate reference system places otherwise correct coordinates in the wrong world, detected by comparing the authority code. An undeclared or absent nodata value causes fill pixels to be counted as measurements, dragging every statistic, detected by asserting both the declared value and its actual presence in the array. A wrong dtype causes arithmetic to wrap rather than saturate, detected by asserting the dtype exactly rather than by checking that values look reasonable. A wrong band count or order makes index-based access read the wrong band, detected by asserting band descriptions rather than positions. A block size differing from production changes how windowed reads behave, detected by asserting the creation options after the file is written. PROPERTY WHAT BREAKS ASSERTION THAT CATCHES IT geotransform every overlay is displaced compare the six coefficients CRS right numbers, wrong world compare the authority code nodata fill counted as measurement declared value AND present in the array dtype arithmetic wraps silently assert the dtype exactly band count / order index access reads the wrong band assert descriptions, not positions block size windowed reads behave differently assert creation options after writing

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 s1,s2s_1, s_2 and origins o1,o2o_1, o_2, alignment requires s1=s2s_1 = s_2 and (o1o2)mods1=0(o_1 - o_2) \bmod s_1 = 0 in both axes, within a tolerance chosen from the coordinate magnitudes rather than from intuition.

Aligned, shifted, and differently-scaled grids Three pairs of overlaid grids. In the aligned pair the two grids share a pixel size and an origin offset that is an exact multiple of that size, so the cell boundaries coincide and an overlay is a direct cell-to-cell operation preserving every value. In the shifted pair the pixel sizes match but the origins differ by a fraction of a cell, so the boundaries interleave and any overlay must resample, changing values without raising an error. In the rescaled pair the pixel sizes themselves differ, so no shift can bring the boundaries into correspondence and resampling is unavoidable. A caption records that only the first case permits an exact comparison. Aligned same size, offset a whole cell overlay is exact Shifted same size, fractional offset every overlay resamples Rescaled different pixel size no offset can fix it Only the first case permits an exact comparison. In the other two, every value a test reads has been interpolated — silently, and by a method nobody chose deliberately.

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.

The same band, three ways of counting it Three computations over one band. In the first, the declared nodata value is masked out, so the mean is taken over valid cells only, reports a plausible measurement, and the valid-cell count matches what the contract expects. In the second, the fill value of minus nine thousand nine hundred and ninety-nine is treated as data, so the mean is dragged far below any physical value and the count silently includes fill cells. In the third, the fill value is zero rather than a sentinel, so the mean is merely lower than expected rather than obviously wrong, which is the case that survives review. A closing note states that asserting the valid-cell count alongside the value is what distinguishes the first case from the third. fill masked mean over valid cells only mean = 14.2 valid cells = 8 412 matches the contract fill = −9999, unmasked sentinel counted as measurement mean = −1 204.7 cells = 10 000 obviously wrong — caught early fill = 0, unmasked zero counted as measurement mean = 11.9 cells = 10 000 merely low — survives review The third column is the dangerous one: the value is plausible, so only the cell count reveals that the statistic was computed over the wrong population. Assert the valid-cell count alongside every statistic. It costs one line and it is the only thing separating column one from column three.

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

  1. 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.
  2. Unsigned dtype arithmetic. Subtracting two uint8 bands 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.