Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map/tests/lod_ladder_vs_pyramid.rs

//! **THE LADDER AND THE PYRAMID NOW STEP TOGETHER — and this is the law that keeps
//! them that way.**
//!
//! There are **two ladders**, sharing one rule ([`lod_step`], `kalm-2`) and not one list:
//!
//! * the **cartographic tier** — [`LOD_TIER_ZOOMS`] `= [0, 6, 9, 12, 15]`, which decides
//!   what is DRAWN (`gpu/cull.wgsl`: a way draws only when `lod <= viewport_lod`);
//! * the **tile pyramid** — `[6, 9, 12, 15]` on the live Sweden extract, which decides
//!   what is FETCHED.
//!
//! # What this file used to assert, and why it was right to
//!
//! Until 2026-08-25 the cartographic ladder was `[0, 6, 12, 15]` and the two lists
//! differed by exactly one rung — **z9**. This file existed to state that as a
//! measurement rather than an aesthetic (GFX_V2 §V2.10 C: *"whether that costs anything
//! is a MEASUREMENT nobody has taken"*), and it asserted the gap was there:
//!
//! > At camera **9, 10 and 11** the fetch steps up to the z9 rung and the draw gate does
//! > **not move** — `view_lod_tier` returns the same `Region` it returned at camera 6.
//!
//! MEASURED, and this is the number that decided it: **the z9 rung is 23.16× the bytes
//! of z6** (442 768 951 B against 19 113 939 B, on the live Sweden extract) **for the
//! same drawn tier**. Cut, stored, shipped over the wire, decoded — and then rendering
//! what the rung below it already rendered.
//!
//! # The decision
//!
//! This file laid out three options and said which was whose call. **The owner took
//! option 1** (2026-08-25): add the rung — `LOD_TIER_ZOOMS = [0, 6, 9, 12, 15]`, a fifth
//! [`Lod`] variant ([`Lod::District`]), and a pass over the style rules that assign one.
//! `highway=tertiary`/`tertiary_link` and `waterway=canal` moved onto it; `secondary`
//! deliberately did not. The accepted cost, stated so it is not re-litigated: **tertiary
//! roads no longer draw between camera 6 and camera 9.**
//!
//! Option 2 (move the Region step to 9) and option 3 (accept and document) are recorded
//! in the history of this file rather than deleted, because the reasoning that ranked
//! them is what makes option 1 legible.
//!
//! # The law now
//!
//! The assertion is therefore INVERTED, and generalised so it is a law rather than a new
//! hand-written expectation:
//!
//! | check | fails on |
//! |---|---|
//! | [`the_two_ladders_step_together_at_every_rung`] | a served rung that changes nothing on screen — the defect this file was written about |
//! | [`every_tier_step_lands_on_a_rung_the_pyramid_serves`] | the converse: a tier declared at a zoom the pyramid does not cut |
//! | [`the_ladder_is_one_strictly_ascending_zoom_per_tier`] | a malformed ladder, so neither law above can pass over one |
//! | [`the_road_hierarchy_steps_across_the_whole_ladder`] | the cartographic split itself, as a table |
//! | [`the_live_pyramid_weighs_each_rung`] | the byte weights, when the extract is on this box |
//!
//! **The second law is the one that matters for whoever adds tier six.** z9 was free —
//! already cut, already served. z8, z10 and z13 are not: each needs a 44 GB re-cut before
//! a tier at that zoom renders anything but the rung below it. A tier declared at an
//! unserved zoom does not fail loudly on its own; it just quietly draws the coarser data.
//!
//! # Why korp's law does not cover this
//!
//! `korp/tests/lod_one_threshold.rs::fetch_level_and_draw_tier_step_together` asserts
//! *"neither side may step without the other"* — for every camera, the tier the painter
//! draws equals the tier of the level the fetch picked. That is necessary and not
//! sufficient: it compares fetch against draw, and on the old ladder they **agreed about
//! ignoring z9**, so it was green throughout. This file states the law over the RUNGS
//! instead of over the cameras, which is why it can see what a camera sweep cannot.
//!
//! [`PYRAMID_ZOOMS`] is a copy of korp's `map_tiles::DEFAULT_TILE_ZOOMS` and cannot be
//! read from the original — korp depends on facett, not the reverse. korp's test reads
//! both originals and is what catches drift in the copy.
//!
//! Run: `cargo test -p facett-map --test lod_ladder_vs_pyramid -- --nocapture`

use facett_map::layer::{lod_step, view_lod_tier, LOD_TIER_ZOOMS};
use facett_map::style::{classify_with, Lod, MapPalette, LOD_COUNT};

/// The live Sweden extract's pyramid rungs (`korp`'s recipe `zooms` axis). Not a facett
/// constant — the pyramid belongs to the data, and this file is about the two lists
/// DISAGREEING, so it must be able to state the other one.
const PYRAMID_ZOOMS: [u8; 4] = [6, 9, 12, 15];

/// The slippy range the map is documented over (`layer.rs`: "the slippy range is 0..=22").
const CAMERAS: std::ops::RangeInclusive<u8> = 0..=22;

/// The tier's own name, read off [`Lod`] rather than transcribed. The hand-written
/// `match` this replaces silently mislabelled every tier from index 2 up the moment a
/// variant was inserted — it said "City" for what had become `District`.
fn tier_name(t: u32) -> String {
    format!("{:?}", Lod::from_u32(t))
}

/// ── THE LAW: every rung the pyramid serves must change what is drawn ────────────
///
/// Asserted as a set, not as a hand-written expectation, so it stays true under a change
/// to either list rather than merely under the change somebody remembered to update.
#[test]
fn the_two_ladders_step_together_at_every_rung() {
    let mut rows = Vec::new();
    for z in CAMERAS {
        let zf = f32::from(z);
        rows.push((z, lod_step(&PYRAMID_ZOOMS, zf), view_lod_tier(zf)));
    }

    // Cameras where the fetch stepped up but the draw gate did not.
    let mut wasted: Vec<u8> = Vec::new();
    for w in rows.windows(2) {
        let (_, prev_rung, prev_tier) = w[0];
        let (z, rung, tier) = w[1];
        if rung > prev_rung && tier == prev_tier {
            wasted.push(z);
        }
    }

    eprintln!("EMIT-LOD camera | pyramid rung | cartographic tier");
    for (z, rung, tier) in &rows {
        let step_marker = if wasted.contains(z) { "  <- fetch stepped, draw did not" } else { "" };
        eprintln!(
            "EMIT-LOD   {z:>2}    |     z{:<2}      | {} ({}){step_marker}",
            PYRAMID_ZOOMS[*rung],
            tier_name(*tier),
            tier
        );
    }

    // NON-VACUITY: the walk must have seen both ladders actually move, or "they agree" is
    // a statement about two constant functions.
    let rungs: std::collections::BTreeSet<usize> = rows.iter().map(|r| r.1).collect();
    let tiers: std::collections::BTreeSet<u32> = rows.iter().map(|r| r.2).collect();
    assert_eq!(rungs.len(), PYRAMID_ZOOMS.len(), "every pyramid rung is reachable in 0..=22");
    assert_eq!(tiers.len(), LOD_TIER_ZOOMS.len(), "every cartographic tier is reachable in 0..=22");

    assert!(
        wasted.is_empty(),
        "camera(s) {wasted:?} fetch a finer pyramid rung and draw the tier the rung below \
         already drew. That level is cut, stored, shipped over the wire and decoded to \
         change nothing on screen — z9 cost 23.16x the bytes of z6 to do exactly that \
         until 2026-08-25. Either give the rung a cartographic tier (the fix that was \
         taken; see the module doc) or stop cutting it.\n\
         Note korp's `fetch_level_and_draw_tier_step_together` is GREEN on this shape, \
         because fetch and draw agree about ignoring the rung."
    );

    // …and the specific band that used to be flat is not, any more. Cameras 9, 10 and 11
    // fetch z9; they must now draw a tier camera 6 does not.
    let at6 = view_lod_tier(6.0);
    for z in [9.0_f32, 10.0, 11.0] {
        assert_ne!(
            view_lod_tier(z),
            at6,
            "camera {z} fetches the z9 rung and must no longer draw the tier camera 6 draws"
        );
    }
    assert_eq!(at6, Lod::Region as u32, "…camera 6 is the Region tier");
    assert_eq!(view_lod_tier(9.0), Lod::District as u32, "…and camera 9 is the District tier");
}

/// **The converse — a tier may only step where there is data to step onto.**
///
/// This is the direction that catches tier six. Nothing about a tier declared at an
/// unserved zoom fails on its own; it simply renders the coarser rung, which is the
/// defect above arrived at from the other side.
#[test]
fn every_tier_step_lands_on_a_rung_the_pyramid_serves() {
    // The first entry is the floor (`0`, the country tier) and is deliberately NOT a
    // pyramid rung: a camera coarser than the whole pyramid clamps UP to the coarsest
    // level, because that is the only data out there. Every entry AFTER it is a step, and
    // a step needs somewhere to land.
    let unserved: Vec<u8> =
        LOD_TIER_ZOOMS[1..].iter().copied().filter(|z| !PYRAMID_ZOOMS.contains(z)).collect();
    assert!(
        unserved.is_empty(),
        "the ladder steps at camera(s) {unserved:?}, which the pyramid {PYRAMID_ZOOMS:?} \
         does not cut — those tiers render the rung below them and are invisible. z9 was \
         free because it was already cut; z8/z10/z13 each need a re-cut first."
    );
}

/// **Shape, so neither law above can pass over a malformed ladder.**
#[test]
fn the_ladder_is_one_strictly_ascending_zoom_per_tier() {
    assert_eq!(
        LOD_TIER_ZOOMS.len(),
        LOD_COUNT,
        "the ladder must have exactly one camera zoom per `Lod` tier"
    );
    for (i, w) in LOD_TIER_ZOOMS.windows(2).enumerate() {
        assert!(w[1] > w[0], "the ladder must strictly ascend — entry {} ({}) <= {}", i + 1, w[1], w[0]);
    }
    // And each rung reads back as its own index, or `view_lod_tier` and the array it is
    // built from have drifted apart.
    for (i, &z) in LOD_TIER_ZOOMS.iter().enumerate() {
        assert_eq!(
            view_lod_tier(f32::from(z)),
            i as u32,
            "camera {z} is ladder entry {i} and must read back as tier {i}"
        );
    }
}

/// **THE CARTOGRAPHIC CONSEQUENCE, MEASURED** — which road classes appear at which
/// camera, stated as a table, so the split the owner decided is a number a future lane can
/// check rather than an intention it has to infer.
#[test]
fn the_road_hierarchy_steps_across_the_whole_ladder() {
    let pal = MapPalette::cartographic(true);
    let lod_of = |tags: &[(&str, &str)]| classify_with(tags, &pal).lod;

    // `(name, tags, the LOD the ladder must give it)`.
    type Row = (&'static str, &'static [(&'static str, &'static str)], Lod);
    let rows: [Row; 6] = [
        ("motorway", &[("highway", "motorway")], Lod::Country),
        ("primary", &[("highway", "primary")], Lod::Country),
        ("secondary", &[("highway", "secondary")], Lod::Region),
        ("tertiary", &[("highway", "tertiary")], Lod::District),
        ("residential", &[("highway", "residential")], Lod::City),
        ("footway", &[("highway", "footway")], Lod::Detail),
    ];
    eprintln!("EMIT-LOD class        | tier      | draws from camera");
    for (name, tags, want) in rows {
        let got = lod_of(tags);
        eprintln!("EMIT-LOD {name:<12} | {got:<9?} | {}", LOD_TIER_ZOOMS[got as usize]);
        assert_eq!(got, want, "{name} must classify as {want:?}, got {got:?}");
    }

    // The waterway ladder splits at the same rung and for the same reason: a canal is an
    // infrastructure watercourse at the scale a tertiary road is an infrastructure road.
    // The river above it and the stream below it must NOT move with it, or this is a
    // wholesale re-tiering rather than the one split that was approved.
    assert_eq!(lod_of(&[("waterway", "river")]), Lod::Country, "a river stays country-tier");
    assert_eq!(lod_of(&[("waterway", "canal")]), Lod::District, "a canal moves to the z9 tier");
    assert_eq!(lod_of(&[("waterway", "stream")]), Lod::City, "a stream stays city-tier");

    // AND THE THING THAT MUST NOT HAVE MOVED. Splitting `Lod::Region` was approved for
    // tertiary and canals; taking secondary with them would be a far larger look change
    // than the one decided, and it is the easiest slip to make here.
    assert_eq!(
        lod_of(&[("highway", "secondary")]),
        Lod::Region,
        "secondary roads must STAY at the region tier — only tertiary moved"
    );
    assert_eq!(LOD_TIER_ZOOMS[Lod::Region as usize], 6, "…and the region tier is still camera 6");
}

/// ── WHAT THE FLAT SPOT COST, on the real Sweden pyramid ──────────────────────────
///
/// Reads the live extract when it is on this box and prints the byte weight of each rung
/// beside the tier it buys. Skips loudly rather than silently when the pyramid is absent —
/// a GPU-less box gets a *stated* skip, not a green that looks like a measurement.
///
/// MEASURED 2026-08-24 on oden, `/home/korp/.korp/work/sweden/tiles`:
///
/// | rung | tiles | bytes | ratio to z6 |
/// |---|---|---|---|
/// | z6  | 19        | 19 113 939     | 1.00× |
/// | z9  | 676       | 442 768 951    | **23.16×** |
/// | z12 | 28 488    | 3 884 716 129  | 203.2× |
/// | z15 | 1 560 752 | 43 388 512 204 | 2269.9× |
///
/// (1 589 935 tiles in total, which is the Sweden pyramid's own published tile count — the
/// cross-check that says this walked the whole thing and not a subtree.)
///
/// So the z9 rung WAS **23.16× the bytes of z6 for the same drawn tier**. That is the
/// number "4 tiers against N zooms" was missing, and it turned a redesign into a choice
/// between three concrete options — **option 1 was taken on 2026-08-25**:
///
/// 1. **Add a rung** — `LOD_TIER_ZOOMS = [0, 6, 9, 12, 15]`. Every fetched rung then
///    changes what is drawn. Costs a fifth `Lod` variant and a pass over every style rule
///    that assigns one, which is the expensive option and the one V2.10 implied.
///    **← TAKEN.** `Lod::District`, tertiary + canal moved onto it, `BAKE_FORMAT_GEN`
///    bumped 4 → 5 because the blob header is sized by `LOD_COUNT`.
/// 2. **MOVE the Region step from 6 to 9** — `[0, 9, 12, 15]`. Same four tiers, no `Lod`
///    change, no style pass, and it aligns the two ladders **exactly**: the mutation that
///    red-proves the test above is this exact edit, and it empties the waste list. What it
///    costs is cartographic, not technical — cameras 6, 7 and 8 would draw `Country`
///    instead of `Region`, i.e. a regional overview of Sweden gets national-level content.
///    That is the owner's call, and it is a one-line call.
/// 3. **Accept it and write it down** where the recipe's `zooms` axis is set, so the next
///    person reads "cameras 9–11 pull a finer rung than they draw" as a decision rather
///    than finding it again.
///
/// The ONE thing that is not an option is leaving it undocumented, which is the state
/// V2.10 found it in.
#[test]
fn the_live_pyramid_weighs_each_rung() {
    let root = std::path::Path::new("/home/korp/.korp/work/sweden/tiles");
    if !root.is_dir() {
        eprintln!(
            "EMIT-LOD SKIPPED (STATED, not silent): no live pyramid at {} on this box. The \
             ladder finding above is pure and ran; only the byte weights need the extract.",
            root.display()
        );
        return;
    }

    let mut total_tiles = 0u64;
    let mut weights: Vec<(u8, u64, u64)> = Vec::new();
    for z in PYRAMID_ZOOMS {
        let dir = root.join(format!("z{z}"));
        let (mut tiles, mut bytes) = (0u64, 0u64);
        let mut stack = vec![dir.clone()];
        while let Some(d) = stack.pop() {
            let Ok(entries) = std::fs::read_dir(&d) else { continue };
            for e in entries.flatten() {
                let p = e.path();
                match e.file_type() {
                    Ok(t) if t.is_dir() => stack.push(p),
                    Ok(t) if t.is_file() => {
                        tiles += 1;
                        bytes += e.metadata().map(|m| m.len()).unwrap_or(0);
                    }
                    _ => {}
                }
            }
        }
        total_tiles += tiles;
        weights.push((z, tiles, bytes));
    }

    // A rung that read zero files means the walk failed (permissions, most likely), and a
    // zero-weight table would read exactly like "this rung is free".
    if weights.iter().any(|w| w.1 == 0) {
        eprintln!(
            "EMIT-LOD SKIPPED (STATED): the pyramid is present but at least one rung read \
             ZERO files — almost certainly not readable as this user. Weights: {weights:?}"
        );
        return;
    }

    let base = weights[0].2.max(1);
    eprintln!("EMIT-LOD rung | tiles | bytes | x z6 | tier that camera draws");
    for (z, tiles, bytes) in &weights {
        eprintln!(
            "EMIT-LOD  z{z:<2} | {tiles:>9} | {bytes:>14} | {:>8.2}x | {}",
            *bytes as f64 / base as f64,
            tier_name(view_lod_tier(f32::from(*z)))
        );
    }
    eprintln!("EMIT-LOD total tiles: {total_tiles}");

    // THE ASSERTION, and it is about the shape rather than the numbers — the extract is
    // allowed to be re-cut, but a finer rung that weighs no more than a coarser one means
    // the walk measured something other than the pyramid.
    for w in weights.windows(2) {
        assert!(
            w[1].2 > w[0].2,
            "rung z{} must weigh more than z{} ({} vs {} bytes) — a pyramid whose finer \
             rung is lighter was not walked correctly",
            w[1].0,
            w[0].0,
            w[1].2,
            w[0].2
        );
    }

    // And what the rung now buys, as a number. The weight is why the flat spot was worth
    // fixing rather than documenting; keeping the ratio printed is what would make a
    // REGRESSION (the tier being taken away again) visible as a cost and not just as a
    // failing assert.
    let z6 = weights.iter().find(|w| w.0 == 6).expect("z6 rung").2;
    let z9 = weights.iter().find(|w| w.0 == 9).expect("z9 rung").2;
    eprintln!(
        "EMIT-LOD z9 IS EARNED: camera 9-11 fetches z9 ({:.2}x the bytes of z6) and now \
         draws {} where camera 6 draws {}",
        z9 as f64 / z6 as f64,
        tier_name(view_lod_tier(9.0)),
        tier_name(view_lod_tier(6.0))
    );
    assert!(
        z9 > z6 * 2,
        "the z9 rung must be materially heavier than z6 for its tier to be worth having \
         ({z9} vs {z6})"
    );
    assert_ne!(
        view_lod_tier(9.0),
        view_lod_tier(6.0),
        "…and those bytes must buy a different tier, or the flat spot is back"
    );
}