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

nordisk/facett

facett-demo/src/popgraph.rs

//! **knut-inspired population graph** — the Swedish 11-million-person graph
//! database (the 12 labels A–L from `knut-popviz` / `simulate_population.py`)
//! turned into facett [`Scene`]s that DRIVE the demo's graph components
//! (`graph` / `graphview` / `graph3d` / `depgraph`). Self-contained: no FalkorDB,
//! no network, no filesystem — works natively AND in the wasm browser demo.
//!
//! Two scenes:
//! * [`schema_scene`] — the 12-label **meta-graph** (the data model): each label a
//!   node sized/coloured by its scale-1.0 population, edges = the relationship
//!   types a `Person` participates in. The "11M people graph DB" at a glance.
//! * [`population_scene`] — a deterministic **sampled instance graph**: N people,
//!   each wired to an Address (household) + a probabilistic mix of Company / Debt /
//!   Loan / Vehicle / Property / PoliceRecord / … ties, community-coloured by a
//!   synthetic kommun. Renders as a force-directed social/financial web.

use egui::Color32;
use facett_core::Scene;

/// The 12 knut population labels with their scale-1.0 counts — mirrors
/// `knut_popviz::LABELS` (`crates/knut-popviz/src/lib.rs`) / `simulate_population.py`.
pub const LABELS: [(&str, i64); 12] = [
    ("Person", 11_000_000),
    ("Address", 6_000_000),
    ("Company", 1_200_000),
    ("Debt", 5_000_000),
    ("Loan", 8_000_000),
    ("TaxRecord", 60_000_000),
    ("KfmCase", 2_000_000),
    ("PoliceRecord", 3_000_000),
    ("FkBenefit", 10_000_000),
    ("AfRecord", 3_000_000),
    ("Vehicle", 8_000_000),
    ("Property", 4_000_000),
];

/// Relationship types a `Person` participates in (src-label → dst-label), the
/// edges of the schema meta-graph and the wiring rules of the instance sampler.
/// `(dst_label_index, edge_probability_per_person)`.
const PERSON_TIES: [(usize, f32); 11] = [
    (1, 0.95),  // Address   — almost everyone has a registered address
    (2, 0.18),  // Company   — owns/works at a company
    (3, 0.30),  // Debt
    (4, 0.45),  // Loan
    (5, 0.99),  // TaxRecord — everyone is taxed
    (6, 0.10),  // KfmCase   — enforcement (Kronofogden)
    (7, 0.12),  // PoliceRecord
    (8, 0.55),  // FkBenefit — social insurance (Försäkringskassan)
    (9, 0.20),  // AfRecord  — employment agency (Arbetsförmedlingen)
    (10, 0.60), // Vehicle
    (11, 0.35), // Property
];

/// Category colour for a label index (identity / asset / financial / enforcement)
/// — distinct hues so the graph reads as a colour-coded data model.
fn label_color(i: usize) -> Color32 {
    match i {
        0 => Color32::from_rgb(0x4f, 0xc3, 0xf7),         // Person — cyan (the hub)
        1 | 11 => Color32::from_rgb(0x81, 0xc7, 0x84),    // Address/Property — green (assets)
        2 => Color32::from_rgb(0xff, 0xb7, 0x4d),         // Company — amber
        10 => Color32::from_rgb(0xa1, 0x88, 0x7f),        // Vehicle — taupe
        3..=5 => Color32::from_rgb(0xff, 0x8a, 0x65),  // Debt/Loan/Tax — orange (financial)
        6 | 7 => Color32::from_rgb(0xe5, 0x73, 0x73),     // KfmCase/Police — red (enforcement)
        8 | 9 => Color32::from_rgb(0xba, 0x68, 0xc8),     // Fk/Af — purple (welfare/work)
        _ => Color32::GRAY,
    }
}

/// The **schema meta-graph**: 12 label nodes + the relationship edges a `Person`
/// has, plus a few cross-links (Address↔Property, Company↔Vehicle, Debt→KfmCase).
/// Node label carries the human count (e.g. `"TaxRecord ·60M"`).
pub fn schema_scene() -> Scene {
    let mut s = Scene::new();
    let ids: Vec<usize> = LABELS
        .iter()
        .enumerate()
        .map(|(i, (name, count))| s.node(format!("{name} ·{}", human(*count)), label_color(i)))
        .collect();
    // Person → each related label.
    for (dst, _p) in PERSON_TIES {
        s.edge(ids[0], ids[dst]);
    }
    // A few cross-relationships so it isn't a pure star (the real model has these).
    s.edge(ids[1], ids[11]); // Address — Property
    s.edge(ids[2], ids[10]); // Company — Vehicle (fleet)
    s.edge(ids[3], ids[6]); // Debt — KfmCase (unpaid debt → enforcement)
    s.edge(ids[2], ids[1]); // Company — Address (registered office)
    s
}

/// A deterministic **sampled instance graph** of `n_persons` people wired to real
/// entity nodes by [`PERSON_TIES`] probabilities, community-coloured into
/// `n_kommun` synthetic municipalities. Deterministic in `seed` (a small LCG — no
/// `rand` dep, so it's identical native ↔ wasm and reproducible for tests).
pub fn population_scene(n_persons: usize, n_kommun: usize, seed: u64) -> Scene {
    let mut s = Scene::new();
    let mut rng = Lcg::new(seed);
    let kommun = n_kommun.max(1);
    for _ in 0..n_persons {
        // Each person belongs to a kommun (community) → its hue.
        let k = (rng.next() as usize) % kommun;
        let pid = s.node("Person", community_color(k, kommun));
        for (dst, p) in PERSON_TIES {
            if rng.unit() < p {
                let did = s.node(LABELS[dst].0, label_color(dst));
                s.edge(pid, did);
            }
        }
    }
    s
}

/// Community hue: evenly spaced around the wheel so kommuner are visually distinct.
fn community_color(k: usize, n: usize) -> Color32 {
    let h = (k as f32 / n.max(1) as f32) * 360.0;
    hsv(h, 0.55, 0.85)
}

/// Compact human count: 11_000_000 → "11M", 1_200_000 → "1.2M", 60_000 → "60k".
fn human(n: i64) -> String {
    let n = n as f64;
    if n >= 1e6 {
        let m = n / 1e6;
        if m.fract().abs() < 0.05 { format!("{m:.0}M") } else { format!("{m:.1}M") }
    } else if n >= 1e3 {
        format!("{:.0}k", n / 1e3)
    } else {
        format!("{n:.0}")
    }
}

/// Minimal HSV→Color32 (s,v in 0..1, h in degrees) — no extra deps.
fn hsv(h: f32, s: f32, v: f32) -> Color32 {
    let c = v * s;
    let hp = (h.rem_euclid(360.0)) / 60.0;
    let x = c * (1.0 - (hp % 2.0 - 1.0).abs());
    let (r, g, b) = match hp as i32 {
        0 => (c, x, 0.0),
        1 => (x, c, 0.0),
        2 => (0.0, c, x),
        3 => (0.0, x, c),
        4 => (x, 0.0, c),
        _ => (c, 0.0, x),
    };
    let m = v - c;
    Color32::from_rgb(
        ((r + m) * 255.0) as u8,
        ((g + m) * 255.0) as u8,
        ((b + m) * 255.0) as u8,
    )
}

/// A tiny deterministic LCG (Numerical Recipes constants) — reproducible sampling
/// without a `rand` dependency, identical on native and wasm.
struct Lcg(u64);
impl Lcg {
    fn new(seed: u64) -> Self {
        Lcg(seed ^ 0x9e37_79b9_7f4a_7c15)
    }
    fn next(&mut self) -> u32 {
        self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
        (self.0 >> 33) as u32
    }
    fn unit(&mut self) -> f32 {
        (self.next() as f32) / (u32::MAX as f32)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn schema_scene_has_all_12_labels_and_person_ties() {
        let s = schema_scene();
        assert_eq!(s.nodes.len(), 12, "all 12 knut labels are nodes");
        // Person has at least its 11 ties (+ cross links don't touch Person count).
        assert!(s.edges.len() >= PERSON_TIES.len(), "Person ties present");
        assert!(s.nodes[0].label.starts_with("Person ·11M"), "Person sized 11M: {}", s.nodes[0].label);
    }

    #[test]
    fn population_scene_is_deterministic_and_community_structured() {
        let a = population_scene(300, 6, 42);
        let b = population_scene(300, 6, 42);
        // Deterministic: same seed → same node/edge counts.
        assert_eq!(a.nodes.len(), b.nodes.len(), "deterministic node count");
        assert_eq!(a.edges.len(), b.edges.len(), "deterministic edge count");
        // 300 persons + their ties → many nodes, more edges than persons.
        assert!(a.nodes.len() > 300, "persons + entity nodes ({})", a.nodes.len());
        assert!(a.edges.len() >= 300, "each person has ties ({})", a.edges.len());
        // A different seed → a different graph (real sampling, not constant).
        let c = population_scene(300, 6, 7);
        assert_ne!(a.edges.len(), c.edges.len(), "seed changes the sample");
    }

    #[test]
    fn human_counts_read_right() {
        assert_eq!(human(11_000_000), "11M");
        assert_eq!(human(1_200_000), "1.2M");
        assert_eq!(human(60_000), "60k");
    }
}