Deep dive

How Plotline works: a 20-value decision code per map cell

The EU deforestation screen asks three questions of each map cell. Plotline identifies the facts those questions read, keeps them as a 20-value code per cell, discards the rest, and verifies that every cell, and so every possible plot boundary, gives the same answer.

In brief
  • The screen reads land or water, JRC's 2020 forest class and the Hansen loss year from 2021. No question reads the canopy percentages, which are 72% of Hansen's original bytes.
  • The decision layer is 6,146 B instead of 623,237 B (99.0% smaller). A plain GeoTIFF of the same codes is already 98.6% smaller, so most of the saving comes from the choice of what to keep. Plotline's encoding makes the layer a further 28% smaller than that GeoTIFF.
  • 744,732 of 744,732 cells decode to the same code as the original layers, so every plot boundary in the region gets identical outputs under the written rule. The check covers every cell, not a sample.
1 · The job

Three questions decide the screen.

Selling into the EU requires showing that a farm plot is deforestation-free: was it forest at the end of 2020, and has that forest been lost since? The check is a map join, repeated when a layer updates. Plotline answers it with three questions about the map cells inside a plot boundary.

QuestionNeedsDoes not need
Q1 Was it forest on 31 Dec 2020?JRC 2020 forest class; land, water or unknownCanopy % in 2000 (Hansen treecover2000)
Q2 Has that 2020 forest been lost since?Q1, plus the Hansen loss year 2021–2025Loss years 2001–2020
Q3 The supplier's own check: any Hansen loss in 2021–23?The Hansen loss year 2021–2025, on all landCanopy %, older loss years

Q3 mirrors the check the supplier's record describes. It uses Hansen alone, so it also counts loss on land that was not forest in 2020. Q1 and Q2 are the EUDR question itself. Answers are mapped observations under a written rule, not legal verdicts.

2 · The core idea

Layer analysis: keep only what the questions read

The layer analysis lists the questions, then the stored facts each one reads. Anything no question reads can be dropped.

Stepper · what the original layers store, and what is read

Step 1 of 6

Bars share one linear scale and one yardstick: zstd level 19 of the raw plane. Separable planes are not additive, so the parts do not sum to a total. The shipped store files are smaller: 38,513 B (screening) and 6,146 B (decision).

Dropped · 1

Canopy percentages

Hansen's treecover2000 records canopy cover in 2000. It is 414,142 of the 576,999 Hansen bytes (72%), and no question reads it.

Dropped · 2

Loss years 2001–2020

These fall before the cut-off, and JRC's 2020 map already gives the forest state on the cut-off date. Earlier loss does not change that state.

Dropped · 3

JRC sub-cell detail

JRC maps at about 9 m. A Hansen cell is about 28 m and nests exactly 3 × 3 JRC cells. The written rule reads the centre cell, so 8 of every 9 JRC values are unused.

CheckedThe proof also tests that these drops change nothing. Inverting the canopy percentages, reversing the 2001–2020 loss years, or inverting every JRC cell that is not a Hansen centre leaves every code unchanged. Missing-data masks differ from unused values: a cell with no valid canopy value is still "unknown", as before.
Detail: the EUDR forest definition, and what no step captures technical

JRC's metadata states the forest definition: land over 0.5 ha with trees over 5 m and canopy over 10%, or trees able to reach those in situ, excluding land predominantly in agricultural or urban use. Cattle, cocoa, coffee, oil palm, rubber and soya plantations are excluded from forest. The cut-off is 31 December 2020. This is a summary; Regulation (EU) 2023/1115 is the authoritative text.

  • Captured, through JRC only: the plantation exclusion and the 10% / 5 m tests. The pipeline relies on JRC's own claim.
  • Captured at year resolution: the cut-off. Loss in 2020 cannot be told from loss on 31 Dec 2020.
  • Not captured by any step: the 0.5 ha minimum area (a Hansen cell is 0.076 ha, a JRC cell 0.0085 ha, and no contiguity test is applied), "trees able to reach the thresholds in situ", and degradation.
  • A proxy: "deforestation" here means mapped tree-cover loss on mapped 2020 forest. Hansen loss includes fire and logging and does not say what the land became.

4,813 JRC-forest cells on the chip carry 2001–2020 Hansen loss. They are kept as forest: JRC is the authority for the cut-off state, and Hansen loss only dates what happened after it.

3 · One byte per cell

Each cell is reduced to one of 20 codes.

Each Hansen cell keeps three small facts, combined into one number from 0 to 19: whether the cell is unknown, water or land; if land, whether it was non-forest, forest or unknown in 2020; and whether it has no loss, or loss in 2021 … 2025.

The count is 2 (unknown, water) + 3 forest states × 6 loss options = 20 codes. The decoder below shows, for a chosen cell or code tile, the code, the two layers that hold it and the questions it answers.

SyntheticDecoder · one invented map cell
Cell is
JRC says, on 31 Dec 2020
Hansen loss
Or select a code
2

This panel shows one cell. A plot's answer counts many cells, and any "unknown" among them prevents a clean "no".

Two layers from the same codes

LayerContractAnswersWhat it forgetsBytes
Screeningeudr-screen.v1Q1, Q2, Q3Canopy %, loss 2001–2020, JRC sub-cell detail38,513
Decisioneudr-decision.v1Q1, Q2All of the above, plus the loss year on land that was not forest in 20206,146
Lossless v1 storehansen-centers.v1Anything the three Hansen bands canNothing (the audit tier)535,012

The decision layer drops the loss year on land that was not forest in 2020. A Hansen-only check still counts that loss, and its cause is not established for any particular cell. The land was not forest in 2020, so its later loss cannot be "2020 forest lost since". The lossless store remains as the audit tier, and the public source releases are pinned by hash, so a rule change can re-derive the layer.

4 · How the codes are stored

Four yes/no sheets and a short list of loss events.

Most cells are land with no loss, like their neighbours, so writing the full code for every cell wastes space. The store splits the codes into four yes/no sheets (bit-planes: unknown, water, forest, forest-state-unknown) and keeps the rare loss events as a sparse list, one year symbol per event.

SyntheticStepper · an invented 8 × 6 grid of codes

The grid is invented; the real region has 744,732 cells.

Detail: integrity checks when the file is read technical

The store is a single file: a short prefix, a CRC-protected header that names the grid, level, encoding and source identities, then the payload. Decoders are bounded: every declared size is checked before anything is allocated. The header, the payload hash and a digest of the decoded raster all have to agree. These checks catch accidents. A deliberate forgery that recomputes every hash is out of scope, and is caught only if the caller pins the genuine file hash.

5 · How much smaller

623,237 bytes become 6,146, mostly through layer analysis.

Each rung drops something and states what it dropped. The hatched bar is a control: a standard GeoTIFF of the same decision codes, with no custom encoding.

Storage ladder · bars to scale

Region: Hansen 3 bands + JRC chip, 34 × 17 km. Tile: Hansen 10N_090E + JRC N10_E90, 1.6 billion cells, every strip processed; the tile figures are storage sizes only, with no queries run at tile scale. On the log scale each tick is 10× the last, so 623 KB and 6 KB both stay visible.

smaller: 623,237 B → 6,146 B (101×)
smaller already with a plain GeoTIFF of the same codes
smaller again with Plotline's encoding than that GeoTIFF (8,532 → 6,146 B)
Summary lineMost of the saving comes from the layer analysis, not the codec. A plain GeoTIFF of the decision codes is already 98.6% smaller. Both are reported.

Two further baselines are less favourable to the decision layer. Against only the files the decision reads (lossyear + datamask + JRC, 209,095 B), the decision store is 97.1% smaller. On the whole tile, a zstd-19 recompression of the originals is 148.9 MB; the decision layer is 94.5% smaller than that, and 98.0% smaller than the original files.

6 · The proof

If every cell matches, every possible plot matches.

A plot's answer is computed from only two things: the codes of the cells whose centres fall inside the boundary, and how many selected centres fall outside the stored region. If the decoded store has the same code as the original layers at every cell, any boundary selects the same cells with the same codes and gets the same answer. Checking 17 plots covers 17 cases; checking every cell covers all boundaries.

SyntheticProof · 112 invented cells, then any boundary
Cells matching the originals: 0 of 112

Corners can also be placed by clicking on the grid (pointer only), then "Close polygon". A cell is selected when its centre is inside the polygon.

ObservationEach cell is compared with the original layers.

Synthetic illustration: both sides match by construction. In the real run the match was measured: 744,732 of 744,732 cells, at both levels. The 112 cells and their codes are invented.

SyntheticNegative control: does the check detect an error?
Check only this plot's answer
Check every cell

Observation

The grid is invented and shows the compact copy. The flipped cell is outlined in red and also stated in words.

What was run

CheckResult
Every cell: decoded store vs the original GeoTIFFs, via an independent float-affine path744,732 / 744,732 at both levels
The 17 supplied plots, every output field17 / 17 at both levels, across the store, registry and table paths
Fuzzed polygons: holes, multipolygons, edges through cell centres, chip edges, water9,829 / 9,829 (171 invalid shapes rejected identically by both paths)
Negative controls: one flipped cell, a moved vertex, a flipped JRC bitAll detected
Dropped detail: inverted canopy %, reversed 2001–2020 loss years, inverted non-centre JRC cells0 codes change (ignored as designed)
Independent validator with exact rational arithmetic and its own reducerConfirmed; 0 mismatches between fast and reference selection on 4,920 validator-generated geometries

Pixel fidelity is 100% on every attribute the decision reads. Dropped attributes are listed rather than called lossless.

Independent validation

A separate validator wrote its own reader, its own exact-arithmetic point-in-polygon test and its own reducer, and never imported the main decision code. It confirmed the headline claims and found hardening problems: an unprotected store header, an overflow path, level mix-ups, memory blow-ups from crafted input, and missing content binding on the table and registry. Each was fixed with a test that fails on the old code, and all 17 validator scripts now exit 0. The one accepted risk is deliberate forgery with recomputed hashes; callers who need authenticity pin the genuine file hash.

Receipts

Every answer carries a receipt: source versions, grid, written rule, input and geometry hashes, and the supplier's exact fields, so the answer can be re-checked. A receipt proves integrity against local evidence, not satellite accuracy.

7 · Three ways to answer

New boundary, registered plot, saved answer

A plot can be answered in three ways, reported separately; no blended "N× faster" figure is quoted. The Performance page covers how the speed was reached.

Path 1

A new boundary

A polygon not seen before. The decision layer and exact scanline selection find its cells and count the codes. Hot time on the 17 plots: 5.2× the retained v1 path. A batch evaluator reaches about 16× on 500 synthetic polygons, when the polygons are evaluated in one call.

Path 2

A registered plot

EUDR plots are registered once and re-checked whenever a layer updates, so a plot's cells are found once and kept in an index. Hot time: 44×, or 83× with a standard-library-only re-check.

Path 3

A saved answer

For registered plots, the answer itself can be stored in a table keyed by plot ID, geometry hash and store hash. A change to any key makes the lookup miss. Hot time about 410×. It is a materialised view and answers only plots it already holds.

ScopeThese are hot times on the 17 plots: same process, layer already loaded. End-to-end time from a fresh process also includes starting Python and importing NumPy. The new-boundary and registered-plot figures answer different jobs and are not averaged.

The registered-plot table is 20,650 B and its cell index 4,096 B, reported as sidecars next to the 6,146 B layer.

8 · The app

The app presents the layers, answers and receipts.

The app is one local program (React and TypeScript on the Python engine). It runs offline: no cloud, no keys, no map-tile service. The map is the Hansen raster, recoloured in the browser, so the display shows the encoded data. No screenshots are shown, because they draw supplier plot outlines.

View 1

Map

Plays the years 2001 to 2025 with loss accumulating, and can overlay the JRC 2020 forest. Selecting a plot shows its ~28 m cells with the centre rule drawn as dots. A side panel sets Declared by supplier beside Mapped by Plotline and ends with a question for a human reviewer.

View 2

Cohort

All 17 plots by 25 years in a single "loss barcode", plus a declared-versus-mapped matrix. Any cell opens that plot and year, and counts can be exported as CSV.

View 3

Proof

Opens with the framing "Keep the decision, not the data": storage, identical answers, registered-plot speed, the ladder, the layer analysis and the GeoTIFF control. Below it is the lossless audit tier with 17/17 identical, 100% pixel fidelity and the losing candidates.

View 4

Live query

Takes a new region, an exact plot or a rectangle and returns a fresh original-versus-compact answer with cell evidence and a downloadable receipt. It needs the local engine service; the other three views work offline from a frozen export.

Everywhere

Receipts

From any plot, a receipt shows readable fields (versions, grid, rule, hashes) plus the raw JSON, and can be downloaded.

Everywhere

Notes

A local analyst note per plot, kept in the browser only. Unknown is shown hatched and never as zero; no screen shows a compliance badge.

9 · Negative results and design limits

A learned codec selector did not beat a fixed codec.

Before the decision layers, a small learned selector picked a codec per map patch. On a sealed held-out region it saved 10,417 B (1.17%) among the candidates it could choose from, but a fixed, tuned GeoTIFF at the same level was 10,217 B smaller than the selector, and when it could pick between levels 3, 9 and 19 it lost 400 B to a fixed level 19. It was not integrated. The saving comes from the layer analysis, not from a model.

Two other results: a tuned standard GeoTIFF beat the lossless v1 store by 0.48%, and the early bit-packing candidates were sometimes bigger than the compressed originals.

Design limits

  • Edge cells are all-or-nothing. Under the centre rule a cell counts for a plot when its centre lies inside the boundary; cells are about 0.076 ha. Partial edge coverage and the 0.5 ha minimum area are not modelled.
  • JRC is sampled once per Hansen cell, at the centre of its 3 × 3 block. The alternatives (majority, any forest) give the same decision for all 17 plots, but only one plot has mapped forest.
  • The layer is rule-specific. A changed rule or an added layer means re-deriving from the lossless audit tier: seconds per region, about 7 CPU-minutes per tile (estimated).
  • The largest speed-ups need registered plots. The 44× to 410× figures apply to plots registered in advance; a new boundary gets 5.2×.
  • Storage was measured on a whole tile and query timing on one region.