osm-katana/tests/rung15_measure.rs
//! **Row 23, the MEASUREMENT — what does the z13–z15 city camera actually pay, and what
//! would each candidate fix buy?**
//!
//! # Why this runs before any code changes
//!
//! `GENERALISED_RUNGS` is `[6, 9, 12]`, so a camera at lod 13–15 has no rung column and
//! reads full-precision `geometry`: **25.64 MB / 365 163 ways / 1.64 M vertices / 241 ms
//! at Stockholm** (audit A, 2026-09-05). Two fixes are on the table and they are not
//! equivalent:
//!
//! * **add rung 15** — a fourth derived column, Douglas–Peucker at
//! `360/(2048·2^15)` ≈ 5.4e-6° ≈ **0.6 m**.
//! * **re-encode as i32 microdegrees** — 8 bytes per vertex instead of 16, at a
//! quantisation of 1e-6° ≈ **0.11 m**, which is *finer than the z15 epsilon itself*.
//!
//! `shared::schema::GENERALISED_RUNGS`' own doc asserts the first one is worthless —
//! *"running Douglas–Peucker at that tolerance over Swedish OSM geometry moves almost
//! nothing while costing … a byte-for-byte duplicate of the largest column in the file"*
//! — but that sentence carries **no number**, and it was written before `ff1cd6d` made
//! the kernel metric. The cos(lat) correction changes exactly this: at 60°N a
//! north–south deviation is now scaled by 0.5, so z15 DP removes *more* than it did when
//! the claim was made. A claim with no measurement, invalidated by a later commit, is
//! not a reason to skip a fix — so this measures it.
//!
//! Read-only against the live Sweden cut. It writes nothing and swaps nothing.
//!
//! Run: `cargo test -p osm-katana --release --test rung15_measure -- --ignored --nocapture`
use std::path::Path;
use arrow::array::{Array, BinaryArray, UInt8Array};
use parquet::arrow::ProjectionMask;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
const LIVE: &str = "/home/korp/.korp/work/sweden/ways.parquet";
/// Central Stockholm at lod 14 — the camera the audit priced.
const VIEW: (f64, f64, f64, f64) = (17.95, 59.28, 18.20, 59.40);
/// The quantisation an i32-microdegree encoding would impose, in degrees.
const MICRODEGREE: f64 = 1e-6;
/// A WKB blob re-encoded with i32 microdegree coordinates instead of f64 pairs.
///
/// Only the SIZE is computed, not the bytes: the question here is what the wire would
/// weigh, and building a second encoder before knowing whether it is worth having is the
/// thing this measurement exists to avoid.
///
/// The layout priced is the honest one — the same headers WKB already carries (1 byte
/// order + 4 type + 4 count per geometry, recursively) with each coordinate PAIR going
/// from 16 bytes to 8.
fn microdegree_bytes(wkb: &[u8], verts: usize, geoms: usize) -> usize {
// Every header byte survives; only the coordinate payload halves.
let coord_bytes = verts * 16;
let headers = wkb.len().saturating_sub(coord_bytes);
headers + verts * 8 + geoms * 0
}
/// Vertices and sub-geometry count of a decoded shape.
fn shape_size(wkb: &[u8]) -> Option<(usize, usize)> {
let shape = osm_katana::shared::geometry::decode_wkb(wkb)?;
let mut verts = 0usize;
let mut geoms = 0usize;
match &shape {
osm_katana::shared::geometry::WkbShape::Point(_) => {
verts += 1;
geoms += 1;
}
osm_katana::shared::geometry::WkbShape::Lines(runs) => {
geoms += runs.len();
for r in runs {
verts += r.len();
}
}
osm_katana::shared::geometry::WkbShape::Polys(polys) => {
for (outer, holes) in polys {
geoms += 1 + holes.len();
verts += outer.len();
for h in holes {
verts += h.len();
}
}
}
}
Some((verts, geoms))
}
fn bbox_of(wkb: &[u8]) -> Option<(f64, f64, f64, f64)> {
let c = osm_katana::shared::geometry::decode_coords(wkb)?;
if c.is_empty() {
return None;
}
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for (x, y) in c {
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
Some((x0, y0, x1, y1))
}
fn intersects(b: (f64, f64, f64, f64)) -> bool {
!(b.2 < VIEW.0 || b.0 > VIEW.2 || b.3 < VIEW.1 || b.1 > VIEW.3)
}
/// ★ **The number row 23's decision turns on.** Not a gate — a measurement, printed.
#[test]
#[ignore = "reads korp's live Sweden cut; run with --ignored --release"]
fn what_the_city_camera_pays_and_what_each_fix_would_save() {
let p = Path::new(LIVE);
assert!(p.exists(), "{LIVE} is not on this box");
let file = std::fs::File::open(p).unwrap();
let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
let schema = builder.parquet_schema();
// Project only what is needed: `geometry` and `min_zoom`. Pulling `tags` would
// dominate the read and has nothing to do with the question.
let mut leaves = Vec::new();
for (i, col) in schema.columns().iter().enumerate() {
let n = col.path().string();
if n == "geometry" || n == "min_zoom" {
leaves.push(i);
}
}
let mask = ProjectionMask::leaves(schema, leaves);
let reader = builder.with_projection(mask).with_batch_size(8_192).build().unwrap();
let eps15 = osm_katana::shared::generalise::generalisation_eps(15);
let eps12 = osm_katana::shared::generalise::generalisation_eps(12);
let mut rows = 0usize;
let (mut v_full, mut b_full) = (0usize, 0usize);
let (mut v_z15, mut b_z15) = (0usize, 0usize);
let mut b_micro = 0usize;
let mut decode_fail = 0usize;
for batch in reader {
let batch = batch.unwrap();
let g = batch
.column_by_name("geometry")
.unwrap()
.as_any()
.downcast_ref::<BinaryArray>()
.unwrap();
let mz = batch
.column_by_name("min_zoom")
.unwrap()
.as_any()
.downcast_ref::<UInt8Array>()
.unwrap();
for i in 0..batch.num_rows() {
if g.is_null(i) || mz.is_null(i) {
continue;
}
// What a lod-14 camera admits: the rung must be at or below the camera.
if mz.value(i) > 14 {
continue;
}
let w = g.value(i);
let Some(bb) = bbox_of(w) else { continue };
if !intersects(bb) {
continue;
}
rows += 1;
let Some((verts, geoms)) = shape_size(w) else {
decode_fail += 1;
continue;
};
v_full += verts;
b_full += w.len();
b_micro += microdegree_bytes(w, verts, geoms);
if let Some(s15) = osm_katana::shared::generalise::generalise_wkb(w, eps15) {
if let Some((v, _)) = shape_size(&s15) {
v_z15 += v;
}
b_z15 += s15.len();
} else {
v_z15 += verts;
b_z15 += w.len();
}
}
}
let pct = |a: usize, b: usize| {
if b == 0 { 0.0 } else { 100.0 * (b as f64 - a as f64) / b as f64 }
};
println!("\n── row 23 · lod 14 Stockholm, live cut, min_zoom <= 14 ──");
println!(" rows in view {rows} (decode failures {decode_fail})");
println!(" z15 eps {eps15:.4e}° ≈ {:.2} m", eps15 * 111_320.0);
println!(" z12 eps {eps12:.4e}° ≈ {:.2} m", eps12 * 111_320.0);
println!(" microdegree quantum {MICRODEGREE:.0e}° ≈ {:.3} m", MICRODEGREE * 111_320.0);
println!(" vertices full {v_full}");
println!(" vertices after z15 {v_z15} ({:.1} % removed)", pct(v_z15, v_full));
println!(" BYTES full {b_full} ({:.2} MB)", b_full as f64 / 1e6);
println!(" BYTES z15 column {b_z15} ({:.2} MB, {:.1} % saved)", b_z15 as f64 / 1e6, pct(b_z15, b_full));
println!(" BYTES microdeg {b_micro} ({:.2} MB, {:.1} % saved)", b_micro as f64 / 1e6, pct(b_micro, b_full));
println!(
" BYTES both {} ({:.2} MB, {:.1} % saved)",
b_z15 / 2,
(b_z15 / 2) as f64 / 1e6,
pct(b_z15 / 2, b_full)
);
println!("── the row's gate is >= 40 % ──\n");
assert!(rows > 1_000, "only {rows} rows in the viewport — the projection or the bbox is wrong");
}
/// **The number that actually matters: COMPRESSED bytes off the footer.**
///
/// The measurement above prices *serialized* WKB. What a camera pulls is the
/// **zstd-compressed column chunk**, and the two are not proportional — a `f64`
/// coordinate whose low mantissa bits are noise is nearly incompressible, and the same
/// coordinate rounded to the rung's own precision is not.
///
/// That opens a third candidate the row's brief does not name, and it is the one that
/// costs no reader change anywhere:
///
/// | candidate | what it is | who has to change |
/// |---|---|---|
/// | **A** rung 15 | a fourth derived column, DP at 0.6 m | osm-katana + korp's four-variant level enum |
/// | **B** i32 microdegrees | 8 bytes/vertex instead of 16 | osm-katana **and korp's WKB decoder** |
/// | **C** quantise, keep f64 WKB | round coordinates to 1e-6°, still valid WKB | osm-katana **only** |
///
/// C is lossless against the rung it serves — 1e-6° is 0.11 m and the z15 epsilon is
/// 0.60 m, so the quantum is five times finer than the tolerance the column is cut at.
/// It is measured here rather than argued, because "zstd will like it" is exactly the
/// kind of claim `GENERALISED_RUNGS`' own doc made without a number.
#[test]
#[ignore = "reads korp's live Sweden cut; run with --ignored --release"]
fn what_each_candidate_costs_in_bytes_actually_pulled() {
use std::sync::Arc;
use arrow::array::{ArrayRef, BinaryBuilder};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
let p = Path::new(LIVE);
assert!(p.exists(), "{LIVE} is not on this box");
// Collect the viewport's geometry once.
let file = std::fs::File::open(p).unwrap();
let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
let schema = builder.parquet_schema();
let leaves: Vec<usize> = schema
.columns()
.iter()
.enumerate()
.filter(|(_, c)| {
let n = c.path().string();
n == "geometry" || n == "min_zoom"
})
.map(|(i, _)| i)
.collect();
let mask = ProjectionMask::leaves(schema, leaves);
let reader = builder.with_projection(mask).with_batch_size(8_192).build().unwrap();
let mut geoms: Vec<Vec<u8>> = Vec::new();
for batch in reader {
let batch = batch.unwrap();
let g = batch.column_by_name("geometry").unwrap().as_any().downcast_ref::<BinaryArray>().unwrap();
let mz = batch.column_by_name("min_zoom").unwrap().as_any().downcast_ref::<UInt8Array>().unwrap();
for i in 0..batch.num_rows() {
if g.is_null(i) || mz.is_null(i) || mz.value(i) > 14 {
continue;
}
let w = g.value(i);
if bbox_of(w).is_some_and(intersects) {
geoms.push(w.to_vec());
}
}
}
assert!(geoms.len() > 1_000, "only {} rows collected", geoms.len());
let eps15 = osm_katana::shared::generalise::generalisation_eps(15);
/// Round every coordinate of a WKB blob to the microdegree grid, in place, keeping
/// the format byte-for-byte identical in LAYOUT. Decode/re-encode through the
/// crate's own codec so this cannot quietly produce something the reader would
/// reject.
fn requantise(wkb: &[u8], quantise: bool) -> Option<Vec<u8>> {
use osm_katana::shared::geometry::{WkbShape, decode_wkb, encode_wkb_multilinestring_into, encode_wkb_multipolygon_into, encode_wkb_point};
let q = |v: f64| if quantise { (v * 1e6).round() / 1e6 } else { v };
Some(match decode_wkb(wkb)? {
WkbShape::Point(p) => encode_wkb_point(q(p.0), q(p.1)),
WkbShape::Lines(runs) => {
let r: Vec<Vec<(f64, f64)>> =
runs.iter().map(|r| r.iter().map(|&(x, y)| (q(x), q(y))).collect()).collect();
let mut b = Vec::new();
encode_wkb_multilinestring_into(&r, &mut b);
b
}
WkbShape::Polys(polys) => {
let ps: Vec<(Vec<(f64, f64)>, Vec<Vec<(f64, f64)>>)> = polys
.iter()
.map(|(o, hs)| {
(
o.iter().map(|&(x, y)| (q(x), q(y))).collect(),
hs.iter().map(|h| h.iter().map(|&(x, y)| (q(x), q(y))).collect()).collect(),
)
})
.collect();
let mut b = Vec::new();
encode_wkb_multipolygon_into(&ps, &mut b);
b
}
})
}
/// Write one geometry column with the packer's own compression and row-group size,
/// and report the compressed bytes of that column off the footer.
fn compressed_bytes(blobs: &[Vec<u8>], name: &str) -> u64 {
let dir = std::env::temp_dir().join(format!("kd-row23-{}-{}", name, std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join(format!("{name}.parquet"));
let schema = Arc::new(Schema::new(vec![Field::new("geometry", DataType::Binary, true)]));
let props = parquet::file::properties::WriterProperties::builder()
.set_compression(parquet::basic::Compression::ZSTD(Default::default()))
.set_max_row_group_row_count(Some(8_192))
.build();
let f = std::fs::File::create(&path).unwrap();
let mut w = parquet::arrow::ArrowWriter::try_new(f, schema.clone(), Some(props)).unwrap();
for chunk in blobs.chunks(8_192) {
let mut b = BinaryBuilder::new();
for g in chunk {
b.append_value(g);
}
let cols: Vec<ArrayRef> = vec![Arc::new(b.finish())];
w.write(&RecordBatch::try_new(schema.clone(), cols).unwrap()).unwrap();
}
w.close().unwrap();
let r = parquet::file::serialized_reader::SerializedFileReader::new(
std::fs::File::open(&path).unwrap(),
)
.unwrap();
let md = parquet::file::reader::FileReader::metadata(&r);
let mut bytes = 0u64;
for i in 0..md.num_row_groups() {
for c in md.row_group(i).columns() {
bytes += c.compressed_size() as u64;
}
}
let _ = std::fs::remove_dir_all(&dir);
bytes
}
let base = compressed_bytes(&geoms, "base");
// **The CONTROL, and it has to come first.** `requantise` decodes and re-encodes
// through this crate's own writers, and those always emit the Multi* spelling — so a
// round trip changes the geometry TYPE and adds a wrapper header per row whether or
// not a single coordinate moves. Without this arm, that cost would be attributed to
// the quantisation and the row would draw the opposite conclusion from the truth.
let roundtrip: Vec<Vec<u8>> = geoms.iter().map(|g| requantise(g, false).unwrap_or_else(|| g.clone())).collect();
let c_rt = compressed_bytes(&roundtrip, "roundtrip");
let quant: Vec<Vec<u8>> = geoms.iter().map(|g| requantise(g, true).unwrap_or_else(|| g.clone())).collect();
assert_eq!(quant.len(), geoms.len(), "requantise() dropped rows");
let c_quant = compressed_bytes(&quant, "quant");
let dp15: Vec<Vec<u8>> = geoms
.iter()
.map(|g| osm_katana::shared::generalise::generalise_wkb(g, eps15).unwrap_or_else(|| g.clone()))
.collect();
let c_dp = compressed_bytes(&dp15, "dp15");
let both: Vec<Vec<u8>> = dp15.iter().map(|g| requantise(g, true).unwrap_or_else(|| g.clone())).collect();
let c_both = compressed_bytes(&both, "both");
let pct = |a: u64| 100.0 * (base as f64 - a as f64) / base as f64;
println!("\n── row 23 · COMPRESSED bytes, lod 14 Stockholm, {} rows ──", geoms.len());
println!(" base `geometry` as served {base:>10} ({:.2} MB)", base as f64 / 1e6);
println!(" A DP at z15 (0.60 m) {c_dp:>10} ({:.2} MB, {:+.1} %)", c_dp as f64 / 1e6, -pct(c_dp));
println!(" C0 CTRL re-encode, NO quantise {c_rt:>10} ({:.2} MB, {:+.1} %)", c_rt as f64 / 1e6, -pct(c_rt));
println!(" C quantised to 1e-6 deg {c_quant:>10} ({:.2} MB, {:+.1} %)", c_quant as f64 / 1e6, -pct(c_quant));
println!(" A+C both {c_both:>10} ({:.2} MB, {:+.1} %)", c_both as f64 / 1e6, -pct(c_both));
// ── WHY C went the wrong way, and what B actually costs ────────────────────
//
// A guess would be worthless here, so: what GRID are the served coordinates
// already on? OSM stores fixed-point nanodegrees, and if the cut's f64s are exact
// multiples of 1e-7 their low mantissa bits are a repeating pattern zstd already
// exploits — in which case rounding through `(v*1e6).round()/1e6` does not
// "quantise" anything, it replaces a clean pattern with the arbitrary mantissa of a
// division. That would explain a candidate making the column BIGGER.
let mut on_1e7 = 0usize;
let mut coords_checked = 0usize;
for g in geoms.iter().take(5_000) {
if let Some(cs) = osm_katana::shared::geometry::decode_coords(g) {
for (x, y) in cs {
for v in [x, y] {
coords_checked += 1;
if ((v * 1e7).round() - v * 1e7).abs() < 1e-3 {
on_1e7 += 1;
}
}
}
}
}
println!(
" grid check: {on_1e7} of {coords_checked} coordinates ({:.2} %) sit on the 1e-7 nanodegree grid",
100.0 * on_1e7 as f64 / coords_checked.max(1) as f64
);
/// **Candidate B** — the coordinates as i32 microdegrees instead of f64 pairs.
///
/// Two spellings, because "8 bytes instead of 16" is only the floor: a coordinate
/// list is a PATH, so consecutive vertices differ by very little, and a
/// delta+zigzag+varint payload is what that fact is worth. Both are measured
/// because the cheap one may already clear the gate and is far less code.
///
/// It reads its coordinates through `decode_wkb`, **not** `decode_coords`. That is
/// not a style choice: `decode_coords` returned `None` for **64 %** of the
/// z15-simplified blobs (35 119 of 98 080 survived a `filter_map`), which silently
/// priced the combined candidate over a third of its population and produced a
/// −77.7 % that was not real. `decode_wkb` decodes all 98 080 of both sets.
fn micro_bytes(wkb: &[u8], delta: bool) -> Option<Vec<u8>> {
use osm_katana::shared::geometry::WkbShape;
let cs: Vec<(f64, f64)> = match osm_katana::shared::geometry::decode_wkb(wkb)? {
WkbShape::Point(p) => vec![p],
WkbShape::Lines(runs) => runs.into_iter().flatten().collect(),
WkbShape::Polys(polys) => polys
.into_iter()
.flat_map(|(o, hs)| o.into_iter().chain(hs.into_iter().flatten()))
.collect(),
};
let mut out = Vec::with_capacity(cs.len() * 8);
// The same headers WKB carries; only the coordinate payload is re-spelled, so
// this is an honest price for the column and not for a different schema.
out.extend_from_slice(&wkb[..9.min(wkb.len())]);
let (mut px, mut py) = (0i32, 0i32);
for (x, y) in cs {
let xi = (x * 1e6).round() as i32;
let yi = (y * 1e6).round() as i32;
if delta {
for d in [xi.wrapping_sub(px), yi.wrapping_sub(py)] {
// zigzag + LEB128
let mut z = ((d << 1) ^ (d >> 31)) as u32;
loop {
let b = (z & 0x7f) as u8;
z >>= 7;
if z == 0 {
out.push(b);
break;
}
out.push(b | 0x80);
}
}
px = xi;
py = yi;
} else {
out.extend_from_slice(&xi.to_le_bytes());
out.extend_from_slice(&yi.to_le_bytes());
}
}
Some(out)
}
let b1: Vec<Vec<u8>> = geoms.iter().map(|g| micro_bytes(g, false).unwrap_or_else(|| g.clone())).collect();
let b2: Vec<Vec<u8>> = geoms.iter().map(|g| micro_bytes(g, true).unwrap_or_else(|| g.clone())).collect();
let b1_dp: Vec<Vec<u8>> = dp15.iter().map(|g| micro_bytes(g, true).unwrap_or_else(|| g.clone())).collect();
// A `filter_map` that silently dropped rows would make every ratio below a
// comparison between different populations — the shape of defect this file exists
// to avoid, so it is refused rather than trusted.
assert_eq!(b1.len(), geoms.len(), "micro_bytes dropped rows from the base set");
assert_eq!(b2.len(), geoms.len(), "micro_bytes dropped rows from the base set");
assert_eq!(b1_dp.len(), dp15.len(), "micro_bytes dropped rows from the DP set");
assert_eq!(dp15.len(), geoms.len(), "the DP set is not the base set");
// And the vertex populations, so a byte ratio can be read against the vertex ratio
// rather than admired on its own.
// `shape_size` uses `decode_wkb`, which decodes both sets completely — counting
// through `decode_coords` here would repeat the very row loss asserted against above.
let vcount = |set: &[Vec<u8>]| -> usize {
set.iter().filter_map(|g| shape_size(g)).map(|(v, _)| v).sum()
};
let (v_base, v_dp) = (vcount(&geoms), vcount(&dp15));
println!(
" vertices: base {v_base} -> after z15 DP {v_dp} ({:.1} % removed)",
100.0 * (v_base as f64 - v_dp as f64) / v_base as f64
);
let c_b1 = compressed_bytes(&b1, "b1");
let c_b2 = compressed_bytes(&b2, "b2");
let c_b2dp = compressed_bytes(&b1_dp, "b2dp");
println!(" B1 i32 microdeg, absolute {c_b1:>10} ({:.2} MB, {:+.1} %)", c_b1 as f64 / 1e6, -pct(c_b1));
println!(" B2 i32 microdeg, delta+varint{c_b2:>10} ({:.2} MB, {:+.1} %)", c_b2 as f64 / 1e6, -pct(c_b2));
println!(" A+B2 DP at z15, then delta {c_b2dp:>10} ({:.2} MB, {:+.1} %)", c_b2dp as f64 / 1e6, -pct(c_b2dp));
// **The rung the camera actually wants.** z15's epsilon (0.60 m) is FINER than one
// lod-14 pixel (1.19 m at the 2048 raster), so candidate A is simplifying to a
// tolerance the camera cannot resolve. Price the coarser neighbours too, or the row
// decides between two options without knowing what the third costs.
for z in [13u8, 14] {
let e = osm_katana::shared::generalise::generalisation_eps(z);
let set: Vec<Vec<u8>> = geoms
.iter()
.map(|g| osm_katana::shared::generalise::generalise_wkb(g, e).unwrap_or_else(|| g.clone()))
.collect();
let c = compressed_bytes(&set, &format!("dp{z}"));
let d: Vec<Vec<u8>> = set.iter().map(|g| micro_bytes(g, true).unwrap_or_else(|| g.clone())).collect();
let cd = compressed_bytes(&d, &format!("dp{z}d"));
println!(
" A(z{z}) DP at {:.2} m {c:>10} ({:.2} MB, {:+.1} %) +delta {:.2} MB ({:+.1} %) verts {}",
e * 111_320.0,
c as f64 / 1e6,
-pct(c),
cd as f64 / 1e6,
-pct(cd),
vcount(&set),
);
}
// **Is the city camera even paying in BYTES?** The audit priced this viewport at
// 241 ms. 4.06 MB off a local NVMe is not 241 ms, so before this row spends a wire
// format on a 20 % byte saving, measure what the time is actually made of.
let t = std::time::Instant::now();
let mut sink = 0usize;
for g in &geoms {
if let Some((v, _)) = shape_size(g) {
sink += v;
}
}
let decode_base = t.elapsed();
let dp13: Vec<Vec<u8>> = geoms
.iter()
.map(|g| {
osm_katana::shared::generalise::generalise_wkb(g, osm_katana::shared::generalise::generalisation_eps(13))
.unwrap_or_else(|| g.clone())
})
.collect();
let t = std::time::Instant::now();
let mut sink13 = 0usize;
for g in &dp13 {
if let Some((v, _)) = shape_size(g) {
sink13 += v;
}
}
let decode_13 = t.elapsed();
println!(
" DECODE base {sink} verts in {:.1} ms · z13 {sink13} verts in {:.1} ms ({:+.1} %)",
decode_base.as_secs_f64() * 1e3,
decode_13.as_secs_f64() * 1e3,
-100.0 * (decode_base.as_secs_f64() - decode_13.as_secs_f64()) / decode_base.as_secs_f64(),
);
println!("── the row's gate is >= 40 % saved ──\n");
}