tf_tree 0.0.5

std facade for the tf_tree transform engine: ergonomic builder, lookups, and Display errors.
Documentation
//! Regenerate `testdata/frozen/sensor_domain.tft` — a frozen arena whose edges
//! carry a **non-zero time domain**.
//!
//! ```sh
//! cargo run -p tf_tree --features shm --example gen_domain_fixture
//! ```
//!
//! # Why this fixture has to exist
//!
//! [`0038`](../../../docs/decisions/0038-the-domain-a-binding-cannot-name.md)
//! step 4's verification is *"a pytest that reproduces step 3 through the Python
//! API"* — open a tree whose edges are not tag 0, plan in that domain, read a
//! transform. **Python cannot build one.** `tf_tree.build` and
//! `tf_tree.open(create=...)` construct their `EdgeCfg` from a capacity alone and
//! never reach `EdgeCfg::domain`; `open_arena(domain=...)` is the *rendezvous*
//! domain, an unrelated `u32`. So every arena reachable from Python is tag 0,
//! and on a tag-0 arena `check_domain_tag` compares 0 against 0 whichever
//! spelling the binding used.
//!
//! The measured consequence, before this file existed: reverting **all six** of
//! `tf_tree_py`'s `Plan`-handle query sites to the pre-`0038`
//! `Stamp::<SystemDomain>` spelling left the Python suite fully green. Six of the
//! seven sites the decision is about were verified by nothing.
//!
//! `docs/PHASE5.md` §2.1 is what makes a file the answer: NORMATIVE that a frozen
//! `.tft` is read by the identical `Plan::at` code as a live arena, with no
//! offline variant of the lookup. `tf_tree.open_file` already reads one, so a
//! committed tag-1 `.tft` closes the gap with **no new API on any surface** —
//! which is why this is a fixture and not a publishing keyword.
//!
//! # Why it is regenerated by a command and checked by a test
//!
//! The same shape as `tf_tree_ingest`'s `gen_zstd_conformance`: the artifact is
//! committed because the test must not depend on a build step, and the generator
//! is committed because an artifact nobody can rebuild is one nobody can fix.
//! `crates/tf_tree/tests/frozen.rs` asserts the committed file's *properties*
//! rather than its bytes — a `.tft` header carries `created_unix_ns`,
//! `creator_pid`, `boot_id` and `instance_uuid`, so two freezes of the same tree
//! are never byte-identical and a `memcmp` gate here would fail on every run.
#![allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::print_stdout,
    clippy::print_stderr
)]

fn main() {
    #[cfg(all(feature = "shm", target_os = "linux"))]
    generate();
    #[cfg(not(all(feature = "shm", target_os = "linux")))]
    {
        eprintln!("gen_domain_fixture needs `--features shm` on Linux: writing a .tft maps memory");
        std::process::exit(1);
    }
}

#[cfg(all(feature = "shm", target_os = "linux"))]
fn generate() {
    use tf_tree::{Capacity, Domain, EdgeCfg, InterpPolicy, SensorDomain, TreeBuilder};

    /// Where the fixture lives, relative to the workspace root.
    const OUT: &str = "testdata/frozen/sensor_domain.tft";

    // Tag 1 on every dynamic edge. That is the whole point of the fixture: it is
    // what makes `check_domain_tag` compare two *different* numbers when a
    // binding gets the tag wrong, and therefore what makes the six Python query
    // sites observable at all.
    let cfg = EdgeCfg::new(Capacity::slots(32))
        .interp(InterpPolicy::ScLerp)
        .domain(SensorDomain::TAG);

    // Small on purpose — a tracked binary fixture should cost kilobytes. Two
    // dynamic edges and one static one is enough to exercise a composed route
    // (so `at_many` folds more than one step) without the ~130 KB
    // `tests/frozen.rs`'s own fixture needs for its chunk-loop argument.
    let tree = TreeBuilder::new()
        .dynamic_edge("map", "odom", cfg)
        .dynamic_edge("odom", "base_link", cfg)
        .static_edge(
            "base_link",
            "lidar",
            &tf_tree::exp_se3([0.1, -0.2, 0.3, 0.4, 0.5, -0.6]),
        )
        .build()
        .unwrap();

    // Poses distinct in every component at every stamp, for the same reason
    // `tests/frozen.rs` says: an identity-valued fixture makes a comparison pass
    // for reasons that have nothing to do with what is being tested.
    for (i, (parent, child)) in [("map", "odom"), ("odom", "base_link")]
        .into_iter()
        .enumerate()
    {
        let p = tree.frame(parent).unwrap();
        let c = tree.frame(child).unwrap();
        let w = tree.claim(c, p).unwrap();
        let seed = 1.0 + i as f64;
        for k in 0..16i64 {
            let t = k as f64 * 0.01 * seed;
            w.push(
                k * 10_000_000, // 10 ms apart, so stamps 0..150 ms
                &tf_tree::exp_se3([
                    0.30 * (t * std::f64::consts::SQRT_2).sin(),
                    0.20 * (t * std::f64::consts::PI).cos(),
                    0.17 * t + 0.05 * seed,
                    1.30 * t + 0.11 * seed,
                    -0.70 * (t * std::f64::consts::E).sin(),
                    0.42 * (t + seed).cos(),
                ]),
            )
            .unwrap();
        }
        // Held for the freeze: releasing would clear the claim record, and a
        // consumer reading this file should see edges that were being written.
        core::mem::forget(w);
    }

    let path = std::path::Path::new(OUT);
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    // `created_unix_ns = 0` rather than the wall clock: the value is recorded in
    // the header and a real timestamp would make every regeneration a diff.
    let header = tree
        .freeze_to(path, Some("gen_domain_fixture"), [0; 32], 0)
        .unwrap();
    println!(
        "wrote {OUT}: {} bytes, format_version {}, layout_hash {:#010x}",
        std::fs::metadata(path).unwrap().len(),
        header.format_version,
        header.layout_hash,
    );
}