diff --git a/Cargo.toml b/Cargo.toml index d2abbb8..05f332c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/ogar-from-schema", "crates/ogar-class-view", "crates/ogar-render-askama", + "crates/ogar-fma-skeleton", ] [workspace.package] diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index f942adc..73921e9 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -64,16 +64,16 @@ use lance_graph_contract::{ ontology::{DisplayTemplate, FieldRef, ObjectView}, }; use ogar_vocab::{ - accounting_account, auth_ory_keto, auth_store, auth_zanzibar, auth_zitadel, - billable_work_entry, billing_party, canonical_concept_id, commercial_document, - commercial_line_item, currency_policy, diagnosis, lab_value, medication, patient, - payment_record, priority, product, project, project_actor, project_attachment, - project_changeset, project_comment, project_custom_field, project_custom_value, - project_enabled_module, project_forum, project_journal, project_member_role, - project_membership, project_message, project_news, project_query, project_relation, - project_repository, project_role, project_status, project_type, project_version, - project_watcher, project_wiki_page, project_work_item, tax_policy, treatment, visit, - vital_sign, Class, + Class, accounting_account, anatomical_structure, auth_ory_keto, auth_store, auth_zanzibar, + auth_zitadel, billable_work_entry, billing_party, bone, canonical_concept_id, + commercial_document, commercial_line_item, currency_policy, diagnosis, joint, lab_value, + medication, patient, payment_record, priority, product, project, project_actor, + project_attachment, project_changeset, project_comment, project_custom_field, + project_custom_value, project_enabled_module, project_forum, project_journal, + project_member_role, project_membership, project_message, project_news, project_query, + project_relation, project_repository, project_role, project_status, project_type, + project_version, project_watcher, project_wiki_page, project_work_item, skeleton, tax_policy, + treatment, visit, vital_sign, }; /// All promoted canonical concepts: `(canonical_concept_name, Class)`. @@ -128,6 +128,11 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("treatment", treatment()), ("visit", visit()), ("vital_sign", vital_sign()), + // ── 0x0AXX — anatomy (FMA reference kinds) ── + ("anatomical_structure", anatomical_structure()), + ("skeleton", skeleton()), + ("bone", bone()), + ("joint", joint()), // ── 0x0BXX — auth (the AuthStore class family, keystone §7) ── ("auth_store", auth_store()), ("auth_zitadel", auth_zitadel()), diff --git a/crates/ogar-fma-skeleton/Cargo.toml b/crates/ogar-fma-skeleton/Cargo.toml new file mode 100644 index 0000000..85bcf3e --- /dev/null +++ b/crates/ogar-fma-skeleton/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ogar-fma-skeleton" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +rust-version.workspace = true +description = "FMA skeletal spine — the hand-curated, clamped convergence-anchor atlas: bones as immutable 16×8-bit Morton-tile family-node addresses (partonomy = spatial prefix), the rigid reference frame every imaging modality (ViT / X-ray / ultrasound × Doppler) registers against." + +[features] +default = [] +serde = ["dep:serde", "ogar-vocab/serde"] + +[dependencies] +ogar-vocab = { path = "../ogar-vocab" } +serde = { workspace = true, optional = true } diff --git a/crates/ogar-fma-skeleton/src/guid.rs b/crates/ogar-fma-skeleton/src/guid.rs new file mode 100644 index 0000000..f4df1a4 --- /dev/null +++ b/crates/ogar-fma-skeleton/src/guid.rs @@ -0,0 +1,450 @@ +//! `guid.rs` — the **brutal, uniform GUID**: every tier is a `[256:256]` +//! `[container : member]` pair. +//! +//! # The one relation, at every scale (operator, 2026-06-23) +//! +//! ```text +//! [Galaxy : planet] [country : city] [school : student] +//! [bodypart : bone] [cm² : mm²] [residue : atom] +//! ``` +//! +//! Every `[256:256]` tier is the *same* relation — **`[container : member]`**: +//! the high byte names the coarse container (a 256-codebook), the low byte the +//! member attached within it (another 256-codebook). It is scale-free: the +//! identical algebra addresses a galaxy's planet, a country's city, a +//! bodypart's bone, or a residue's atom. The GUID is nothing but these tiers +//! stacked: +//! +//! ```text +//! [classid] [HEEL] [HIP] [TWIG] [LEAF] tiers, each [container:member] +//! 0xAA:CC x:y x:y x:y family:identity +//! bytes: 0,1 2,3 4,5 6,7 8,9 (10..16 reserved, deeper refine) +//! ``` +//! +//! Containment manifests **two ways that agree** (D-BOTHCASC): +//! - **HHTL = spatial containment** — the `cm²:mm²` axis (coarse body cell ⊃ +//! fine cell), a 2D-Morton refinement of the rest-pose position. +//! - **LEAF = categorical containment** — the `bodypart:bone` axis (group ⊃ +//! instance). +//! +//! Each tier byte is simultaneously a directly-addressable 256-codebook index +//! **and** a 4×4 Morton pyramid (two nibble-tiles) that can carry +//! perturbation-shader refinement. `tier = level >> 2` is uniform end to end — +//! the leaf is just the last HHTL tier, not a special tail. + +use crate::morton::{KEY_BYTES, tile}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Number of `[256:256]` tiers in the 16-byte key (`KEY_BYTES / 2` = 8). +pub const TIERS: usize = KEY_BYTES / 2; + +/// Tier 0 — the classid (`app:concept`), the routing prefix. +pub const TIER_CLASSID: usize = 0; +/// Tier 1 — HEEL: coarse spatial container:member. +pub const TIER_HEEL: usize = 1; +/// Tier 2 — HIP: finer spatial. +pub const TIER_HIP: usize = 2; +/// Tier 3 — TWIG: finer still. +pub const TIER_TWIG: usize = 3; +/// Tier 4 — LEAF: the categorical `familyNode:identity` (`bodypart:bone`). +pub const TIER_LEAF: usize = 4; + +/// One `[256:256]` tier — a `[container : member]` pair. `container` (the high +/// byte) names the coarse cell; `member` (the low byte) the instance attached +/// within it. Each byte is a 256-codebook index and a 4×4 Morton pyramid. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Tier { + /// The coarse container (high byte) — galaxy / country / bodypart / cm². + pub container: u8, + /// The member attached within the container (low byte) — planet / city / + /// bone / mm². + pub member: u8, +} + +impl Tier { + /// The tier as its 16-bit value (`container << 8 | member`). + #[must_use] + pub const fn as_u16(self) -> u16 { + ((self.container as u16) << 8) | self.member as u16 + } +} + +/// The LEAF tier named in its categorical role: `familyNode:identity` — the +/// `[bodypart : bone]` of the skeleton. `family_node` is the containing group +/// (routing prefix byte); `identity` is the instance attached within it. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LeafTile { + /// The containing family / bodypart / group (a 256-codebook). + pub family_node: u8, + /// The instance attached within the family (a 256-codebook). + pub identity: u8, +} + +/// The 16-byte canonical GUID, read as [`TIERS`] `[container:member]` tiers. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Guid { + bytes: [u8; KEY_BYTES], +} + +impl Default for Guid { + fn default() -> Self { + Self::ZERO + } +} + +impl Guid { + /// The all-zero GUID (the bootstrap / default-everything address — the + /// zero-fallback ladder's base). + pub const ZERO: Self = Self { + bytes: [0u8; KEY_BYTES], + }; + + /// Wrap raw key bytes. + #[must_use] + pub const fn from_bytes(bytes: [u8; KEY_BYTES]) -> Self { + Self { bytes } + } + + /// The raw 16-byte key. + #[must_use] + pub const fn bytes(&self) -> [u8; KEY_BYTES] { + self.bytes + } + + /// Read tier `i` (`0..TIERS`) as a `[container:member]` pair. + #[must_use] + pub const fn tier(&self, i: usize) -> Tier { + Tier { + container: self.bytes[2 * i], + member: self.bytes[2 * i + 1], + } + } + + /// Write tier `i`. + pub const fn set_tier(&mut self, i: usize, t: Tier) { + self.bytes[2 * i] = t.container; + self.bytes[2 * i + 1] = t.member; + } + + /// The classid (tier 0) as a `u16` (`app << 8 | concept` byte order — here + /// the canonical concept, e.g. `0x0A03` for `bone`). + #[must_use] + pub const fn classid(&self) -> u16 { + self.tier(TIER_CLASSID).as_u16() + } + + /// The HEEL spatial tier. + #[must_use] + pub const fn heel(&self) -> Tier { + self.tier(TIER_HEEL) + } + + /// The LEAF tier as `familyNode:identity`. + #[must_use] + pub const fn leaf(&self) -> LeafTile { + let t = self.tier(TIER_LEAF); + LeafTile { + family_node: t.container, + identity: t.member, + } + } + + /// The family-node (the leaf container — `bodypart`). + #[must_use] + pub const fn family_node(&self) -> u8 { + self.bytes[2 * TIER_LEAF] + } + + /// The identity (the leaf member — the specific `bone`), attached within + /// the family node. + #[must_use] + pub const fn identity(&self) -> u8 { + self.bytes[2 * TIER_LEAF + 1] + } + + /// `true` if `self` and `other` are in the **same family**: same `classid` + /// and same leaf `family_node`. This is "identity attached to family node" + /// — the family node groups, the leaf `identity` discriminates within. It + /// is mode-agnostic: in Located mode the spatial HHTL is an *orthogonal* + /// location axis (two same-family bones sit at different positions), so + /// family grouping keys on the categorical leaf byte, not the HHTL prefix. + #[must_use] + pub fn same_family(&self, other: &Self) -> bool { + self.classid() == other.classid() && self.family_node() == other.family_node() + } +} + +impl core::fmt::Debug for Guid { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Guid[")?; + for i in 0..TIERS { + let t = self.tier(i); + let sep = if i == 0 { "" } else { " " }; + write!(f, "{sep}{:02x}:{:02x}", t.container, t.member)?; + } + write!(f, "]") + } +} + +/// Fold an 8-bit-per-axis coronal position `(x, y)` into a spatial +/// `[container : member]` tier: the **coarse** byte (`cm²`) is the top 4 bits +/// of each axis, 2D-Morton-interleaved; the **fine** byte (`mm²`) is the +/// bottom 4 bits. Laterality (the X axis) therefore lives in the even bits of +/// both bytes — recoverable from the address prefix before any value decode. +#[must_use] +pub fn spatial_tier(x: u8, y: u8) -> Tier { + let coarse = (tile((x >> 6) & 3, (y >> 6) & 3) << 4) | tile((x >> 4) & 3, (y >> 4) & 3); + let fine = (tile((x >> 2) & 3, (y >> 2) & 3) << 4) | tile(x & 3, y & 3); + Tier { + container: coarse, + member: fine, + } +} + +/// Encode a 3-axis body position into the Located `(HEEL, HIP)` spatial tier +/// pair — an ArcGIS / Cesium-addressable coordinate reference system. +/// +/// - **HEEL** = coronal `(x : y)` 2D-Morton (anatomical-left × superior) — the +/// plane an X-ray or anterior-ultrasound sweep projects onto. +/// - **HIP** = depth `(z)` — anterior-posterior, the imaging *slice* axis. +/// +/// Prefix containment over `HEEL ++ HIP` is therefore 3-axis spatial +/// containment: a quadkey/octant the GIS stack can `contains` / `buffer` / +/// page by LOD. v0 fence: this is *coronal-tile + depth-slice* (exactly how +/// medical imaging is organised), not yet a single interleaved 3D octree — +/// HIP's odd (Y) bits are reserved for a finer refinement axis. +#[must_use] +pub fn located_heel_hip(x: u8, y: u8, z: u8) -> (Tier, Tier) { + (spatial_tier(x, y), spatial_tier(z, 0)) +} + +/// The **mode** of an HHTL tier — the operator's "heel" distinction +/// (2026-06-23). +/// +/// The *same* `[container:member]` tier algebra reads two ways: +/// +/// - [`Located`](Self::Located) — HEEL is the **literal heel**: a real +/// position (`heel : muscle`). Preserves location, Cesium-style (the tile +/// anchors to an actual coordinate). HHTL = spatial Morton ([`spatial_tier`]). +/// This is the **bone** case — bones ARE the position anchor. +/// - [`Cascade`](Self::Cascade) — HEEL is a classification rung in a scale-free +/// containment cascade (`universe … atom`, `Anatomy … PapillaryMuscle`). +/// **No spatial address** — pure ontology. HHTL = named codebook entries +/// (the self-speaking GUID, `ANAT0001-CARD-HERT-LVNT-PAPMUS-…`). This is the +/// **soft-tissue / muscle** case — classified, then projected onto. +/// +/// Both modes share the LEAF `familyNode:identity` and the same 16-byte key; +/// the mode is a reading of the HHTL block, not a different structure. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum HhtlMode { + /// Spatial / position-preserving (Cesium). The bone case. + Located, + /// Ontological cascade, no spatial address. The muscle case. + Cascade, +} + +/// An ontology tier from two named codebook bytes — `[parentClass : childClass]` +/// (e.g. `[Heart : LeftVentricle]`). The Cascade-mode counterpart of +/// [`spatial_tier`]; it is just a [`Tier`] with the names made explicit. +#[must_use] +pub const fn ontology_tier(parent_class: u8, child_class: u8) -> Tier { + Tier { + container: parent_class, + member: child_class, + } +} + +// ── Relations live in the family-node addressing, NOT a fixed edge block ── +// +// The pre-family-node taxonomy carved relations into a fixed `12 in-family + +// 4 out-of-family` edge block (the papillary-muscle worked example shows it). +// That is **superseded by family nodes** (operator, 2026-06-23). With the +// `[container:member]` tier model, relations ARE the addressing: +// +// - **local relation** ⇔ shared family prefix — `Guid::same_family` (same +// tiers up to the leaf `family_node`); no slots needed, the address says it. +// - **cross relation** ⇔ a reference to another family node's `Guid` (the +// target's address), unbounded — not a fixed-4 inherited-interface carve. +// +// So there is no `EdgeBlock` here. A node's neighbourhood is read from the key +// space (prefix containment + family-node references), keeping the node a pure +// `key + value` block with no special edge taxonomy. + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tier_roundtrips_through_bytes() { + let mut g = Guid::ZERO; + g.set_tier( + TIER_CLASSID, + Tier { + container: 0x0A, + member: 0x03, + }, + ); + g.set_tier( + TIER_LEAF, + Tier { + container: 0x42, + member: 0x07, + }, + ); + assert_eq!(g.classid(), 0x0A03); + assert_eq!(g.family_node(), 0x42); + assert_eq!(g.identity(), 0x07); + assert_eq!( + g.leaf(), + LeafTile { + family_node: 0x42, + identity: 0x07 + } + ); + } + + #[test] + fn same_family_means_identity_attached() { + let mut a = Guid::ZERO; + a.set_tier( + TIER_CLASSID, + Tier { + container: 0x0A, + member: 0x03, + }, + ); + a.set_tier( + TIER_LEAF, + Tier { + container: 0x42, + member: 0x01, + }, + ); + let mut b = a; // same family, different identity + b.set_tier( + TIER_LEAF, + Tier { + container: 0x42, + member: 0x02, + }, + ); + let mut c = a; // different family + c.set_tier( + TIER_LEAF, + Tier { + container: 0x43, + member: 0x01, + }, + ); + assert!(a.same_family(&b), "same family_node, identity attached"); + assert!(!a.same_family(&c), "different family_node"); + assert_ne!(a.bytes(), b.bytes()); + } + + #[test] + fn spatial_tier_encodes_laterality_in_x_bits() { + // Two points differing only in X (laterality) must differ in the tier; + // the difference lives in the even (X) bit-positions. + let left = spatial_tier(0x20, 0x80); + let right = spatial_tier(0xE0, 0x80); + assert_ne!(left, right); + // Same X, differing Y differ too (cranio-caudal). + assert_ne!(spatial_tier(0x80, 0x20), spatial_tier(0x80, 0xE0)); + } + + #[test] + fn uniform_eight_tiers_over_sixteen_bytes() { + assert_eq!(TIERS, 8); + let g = Guid::from_bytes([0xAB; KEY_BYTES]); + for i in 0..TIERS { + assert_eq!( + g.tier(i), + Tier { + container: 0xAB, + member: 0xAB + } + ); + } + } + + #[test] + fn located_and_cascade_share_leaf_and_key_width() { + // Same classid + leaf; HHTL filled two ways. The mode is a reading of + // the HHTL block, not a different structure — both are 16-byte keys + // with the same family:identity leaf. + let classid = Tier { + container: 0x0A, + member: 0x03, + }; + let leaf = Tier { + container: 0x42, + member: 0x07, + }; + + let mut located = Guid::ZERO; // Cesium: HHTL = spatial Morton + located.set_tier(TIER_CLASSID, classid); + located.set_tier(TIER_HEEL, spatial_tier(0xC0, 0x40)); + located.set_tier(TIER_LEAF, leaf); + + let mut cascade = Guid::ZERO; // ontology: HHTL = named codebook path + cascade.set_tier(TIER_CLASSID, classid); + cascade.set_tier(TIER_HEEL, ontology_tier(0x01, 0x02)); // e.g. Heart:LeftVentricle + cascade.set_tier(TIER_LEAF, leaf); + + assert_eq!(located.leaf(), cascade.leaf(), "same family:identity"); + assert_eq!(located.classid(), cascade.classid()); + assert_eq!(located.bytes().len(), cascade.bytes().len()); + assert_ne!(located.heel(), cascade.heel(), "HHTL differs by mode"); + let _ = HhtlMode::Located; + let _ = HhtlMode::Cascade; + } + + #[test] + fn relations_are_family_prefix_not_an_edge_block() { + // Superseding the 12+4 edge taxonomy: a local relation is a shared + // family prefix; a cross relation is a reference to another node's Guid. + let classid = Tier { + container: 0x0A, + member: 0x03, + }; + let mut a = Guid::ZERO; + a.set_tier(TIER_CLASSID, classid); + a.set_tier( + TIER_LEAF, + Tier { + container: 0x42, + member: 0x01, + }, + ); + let mut sibling = a; // same family node, identity attached + sibling.set_tier( + TIER_LEAF, + Tier { + container: 0x42, + member: 0x02, + }, + ); + let mut other_family = a; + other_family.set_tier( + TIER_LEAF, + Tier { + container: 0x99, + member: 0x01, + }, + ); + + assert!( + a.same_family(&sibling), + "local relation = shared family prefix" + ); + assert!(!a.same_family(&other_family)); + // A cross relation is just the other node's address (a Guid), unbounded. + let _cross_ref: Guid = other_family; + } +} diff --git a/crates/ogar-fma-skeleton/src/lib.rs b/crates/ogar-fma-skeleton/src/lib.rs new file mode 100644 index 0000000..ba0b5c7 --- /dev/null +++ b/crates/ogar-fma-skeleton/src/lib.rs @@ -0,0 +1,993 @@ +//! `ogar-fma-skeleton` — the **FMA skeletal spine**: the hand-curated, +//! clamped convergence-anchor atlas. +//! +//! # What this is (operator directive, 2026-06-23) +//! +//! > *"FMA is a must. It must be meticulously optimized in order to later have +//! > stability of the human body — bones not being negotiable. The body is +//! > hardcoded, hand-optimized convergence optimization."* +//! +//! Bones are the only rigid bodies in the human. Soft tissue deforms, breathes, +//! and is noisy; Doppler is velocity, not structure. So the skeleton is not +//! *data being fit* — it is the **boundary condition of the fit**: the rigid, +//! immutable frame onto which the splat-native ultrasound pipeline (and every +//! other modality) registers. Clamping the splat-fit to a hand-curated skeleton +//! makes the convergence well-posed and deterministic — what Class IIa SaMD +//! requires (`OGAR/docs/SPLAT-NATIVE-CUSTOMER.md` §6 litmus). +//! +//! # The two directives, unified +//! +//! 1. **Bones hardcoded & stable.** Each bone is a `kind == Bone` node and is, +//! by definition, a [`Bone::is_clamped_anchor`] — there is no un-clamped +//! bone. Its [`Guid`] classid is immutable (RESERVE-DON'T-RECLAIM); a +//! snapshot test pins it for every bone. +//! 2. **Multi-modal projection.** ViT / X-ray / ultrasound × Doppler are each +//! a forward operator onto the shared splat volume; all of them register +//! *through the bone frame*. X-ray *shows* bone (the natural fiducial); +//! ultrasound returns specularly off bone surfaces; Doppler's view-dependent +//! velocity is the splat SH term. Request 1 (bones stable) is the +//! precondition for Request 2 (cross-modal registration). +//! +//! # The address (per [`guid`]) +//! +//! Each bone's identity is a 16-byte [`Guid`] — a stack of `[256:256]` +//! `[container:member]` tiers (`src/guid.rs`), **Located-primary** (bones ARE +//! the position anchor): +//! +//! - **classid** tier = `ogar_vocab::class_ids::BONE` (`0x0A03`, Anatomy:bone); +//! - **HEEL/HIP** = the rest-pose **3-axis CRS** (HEEL coronal `x:y`, HIP depth +//! `z`) — ArcGIS / Cesium-addressable, so the body drops into a GIS stack as +//! a first-class spatial layer and ViT / ultrasound × Doppler project onto +//! the same coordinate; +//! - **LEAF** = `familyNode:identity` = `[bodypart : bone]` — the categorical +//! family (the group) and the instance attached within it. +//! +//! The family node groups (a bone's parent group is its family; `identity == 0` +//! IS the family node, `identity ≥ 1` are its bones), spatial HHTL locates, and +//! the perturbation pyramid's `(exponent, location)` ride the Morton tiles — +//! all on one key. (The lower-level Morton primitives + the variable-depth +//! [`FamilyAddress`] remain in [`morton`] as the value-side splat-refinement +//! tool.) +//! +//! # Honesty fence +//! +//! Rest-pose coordinates are a **canonical proportional T-pose v0** in a body +//! frame (origin at the sacral promontory; `+X` anatomical-left, `+Y` superior, +//! `+Z` anterior; orientation identity). They carry laterality and +//! cranio-caudal order faithfully — enough for the Morton structure and the +//! invariant tests — but are NOT clinically-precise and are to be refined +//! against a real FMA-aligned reference mesh in the splat-native arc. What is +//! rigorous here is the *structure*: tree integrity, the `[container:member]` +//! tier model, HEEL laterality + HIP depth, family-node grouping with +//! attached identity, globally-unique node keys, unit-quaternion frames, the +//! clamped-anchor invariant, and classid stability. + +#![warn(missing_docs)] +#![forbid(unsafe_code)] + +pub mod guid; +pub mod morton; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +pub use guid::{Guid, HhtlMode, LeafTile, Tier}; +pub use morton::FamilyAddress; + +/// Re-export of the `bone` concept id from the canonical OGAR codebook +/// (`ogar_vocab::class_ids::BONE` = `0x0A03`). It is the classid tier of every +/// bone's [`Guid`] (`0x0A` Anatomy : `0x03` bone). +pub const BONE_CONCEPT: u16 = ogar_vocab::class_ids::BONE; + +/// Our stable atlas identity for a node. Distinct from an FMA id: this is the +/// curated node identity. The FMA id (when confidently known) is carried +/// separately in [`BoneSpec::fma`] as a cross-reference — we do not fabricate +/// FMA ids we are unsure of. +pub type AtlasId = u32; + +/// The FMA axial / appendicular partition — the top split of the skeleton. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] +pub enum Region { + /// Axial skeleton — skull, vertebral column, thoracic cage (the central + /// spine the body hangs off; the dominant X-ray / ultrasound landmark set). + Axial, + /// Upper appendicular — pectoral girdle + arm. + AppendicularUpper, + /// Lower appendicular — pelvic girdle + leg. + AppendicularLower, +} + +/// Whether a node is an actual bone (a clamped convergence anchor) or a pure +/// structural grouping (e.g. "vertebral column") that exists only to shape the +/// partonomy / address prefix. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] +pub enum NodeKind { + /// An actual bone. **Always a clamped anchor** — bones are non-negotiable. + Bone, + /// A structural grouping node (partonomy interior). Not an anchor. + Group, +} + +/// A rigid body-frame transform: translation (metres) + unit-quaternion +/// rotation `(x, y, z, w)`. The rest pose of a bone in the canonical T-pose. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RigidTransform { + /// Position in the body frame: `+X` anatomical-left, `+Y` superior, + /// `+Z` anterior. Origin at the sacral promontory. + pub translation: [f32; 3], + /// Orientation quaternion `(x, y, z, w)`. v0 rest poses use identity. + pub rotation: [f32; 4], +} + +impl RigidTransform { + /// Identity orientation at `position`. + #[must_use] + pub const fn at(position: [f32; 3]) -> Self { + Self { + translation: position, + rotation: [0.0, 0.0, 0.0, 1.0], + } + } + + /// `true` if the rotation is a unit quaternion (within `1e-3`) — the + /// validity condition for a rigid frame. + #[must_use] + pub fn is_valid(&self) -> bool { + let [x, y, z, w] = self.rotation; + let n2 = x * x + y * y + z * z + w * w; + (n2 - 1.0).abs() < 1e-3 && self.translation.iter().all(|v| v.is_finite()) + } + + /// The coronal `(x, y)` centroid used for Morton-tile placement + /// (anatomical-left × superior). This is the plane an X-ray / anterior + /// ultrasound sweep projects onto. + #[must_use] + pub const fn coronal(&self) -> (f32, f32) { + (self.translation[0], self.translation[1]) + } +} + +/// A static atlas row — the hand-curated declaration of one skeletal node. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct BoneSpec { + /// Stable atlas identity (address identity). + pub id: AtlasId, + /// FMA cross-reference (`FMA:xxxxx` numeric) where confidently known. + pub fma: Option, + /// Terminologia Anatomica Latin name. + pub name_la: &'static str, + /// Common English name. + pub name_en: &'static str, + /// Parent node in the partonomy (`None` for the skeletal-system root). + pub parent: Option, + /// Axial / appendicular region. + pub region: Region, + /// Bone vs grouping node. + pub kind: NodeKind, + /// Rest pose in the canonical body frame. + pub rest_pose: RigidTransform, +} + +/// A resolved node: its [`BoneSpec`] plus the Located-primary [`Guid`] derived +/// from the rest pose + partonomy family. +#[derive(Clone, Debug, PartialEq)] +pub struct Bone { + spec: BoneSpec, + guid: Guid, +} + +impl Bone { + /// The stable atlas id. + #[must_use] + pub const fn id(&self) -> AtlasId { + self.spec.id + } + /// The FMA cross-reference, when known. + #[must_use] + pub const fn fma(&self) -> Option { + self.spec.fma + } + /// Latin (Terminologia Anatomica) name. + #[must_use] + pub const fn name_la(&self) -> &'static str { + self.spec.name_la + } + /// English name. + #[must_use] + pub const fn name_en(&self) -> &'static str { + self.spec.name_en + } + /// Parent atlas id. + #[must_use] + pub const fn parent(&self) -> Option { + self.spec.parent + } + /// Region. + #[must_use] + pub const fn region(&self) -> Region { + self.spec.region + } + /// Node kind. + #[must_use] + pub const fn kind(&self) -> NodeKind { + self.spec.kind + } + /// Rest pose. + #[must_use] + pub const fn rest_pose(&self) -> RigidTransform { + self.spec.rest_pose + } + /// The bone's canonical 16-byte [`Guid`] — Located-primary: classid + /// `0x0A03` (Anatomy:bone) · HEEL/HIP = the rest-pose 3-axis CRS · LEAF = + /// `familyNode:identity` (`bodypart:bone`). + #[must_use] + pub const fn guid(&self) -> Guid { + self.guid + } + + /// The HEEL spatial tier (coronal `x:y` plane). + #[must_use] + pub const fn heel(&self) -> Tier { + self.guid.heel() + } + + /// The LEAF tier as `familyNode:identity`. + #[must_use] + pub const fn leaf(&self) -> LeafTile { + self.guid.leaf() + } + + /// The family node — the leaf container (the `bodypart` / group). Bones in + /// the same group share this byte; `identity == 0` IS the group's family + /// node, `identity ≥ 1` are its bones. + #[must_use] + pub const fn family_node(&self) -> u8 { + self.guid.family_node() + } + + /// The instance identity within the family (the leaf member). + #[must_use] + pub const fn identity(&self) -> u8 { + self.guid.identity() + } + + /// The full canonical 16-byte node key (the [`Guid`] bytes). + #[must_use] + pub const fn node_key(&self) -> [u8; morton::KEY_BYTES] { + self.guid.bytes() + } + /// `true` iff this node is a clamped convergence anchor — i.e. an actual + /// bone. **Bones are non-negotiable; there is no un-clamped bone.** + #[must_use] + pub const fn is_clamped_anchor(&self) -> bool { + matches!(self.spec.kind, NodeKind::Bone) + } +} + +/// The resolved skeletal atlas — every node with its derived address. +#[derive(Clone, Debug)] +pub struct Skeleton { + nodes: Vec, +} + +impl Skeleton { + /// Resolve the curated atlas into Located-primary [`Guid`]s: classid + /// `0x0A03` · HEEL/HIP = the rest-pose 3-axis CRS · LEAF = + /// `familyNode:identity`. The family node is the node's parent group + /// (`region nibble · group-index nibble`); the identity is the node's + /// 1-based rank among its siblings (`0` for the group node itself). + #[must_use] + pub fn resolve() -> Self { + use std::collections::HashMap; + let specs = atlas(); + + let region_code = |r: Region| -> u8 { + match r { + Region::Axial => 0, + Region::AppendicularUpper => 1, + Region::AppendicularLower => 2, + } + }; + + // Each group gets a group-index within its region (the family low nibble). + let mut group_index: HashMap = HashMap::new(); + let mut region_counter = [0u8; 3]; + for s in &specs { + if matches!(s.kind, NodeKind::Group) { + let rc = region_code(s.region) as usize; + group_index.insert(s.id, region_counter[rc] & 0x0F); + region_counter[rc] = region_counter[rc].wrapping_add(1); + } + } + + // identity = 1-based rank of a bone among its parent's children; a group + // node takes identity 0 — it IS the family node. + let mut sibling_counter: HashMap, u8> = HashMap::new(); + let mut identity_of = vec![0u8; specs.len()]; + for (pos, s) in specs.iter().enumerate() { + if matches!(s.kind, NodeKind::Bone) { + let c = sibling_counter.entry(s.parent).or_insert(0); + *c = c.wrapping_add(1); + identity_of[pos] = *c; + } + } + + // Body bounding box, for the 8-bit spatial quantize (the CRS extent). + let mut min = [f32::INFINITY; 3]; + let mut max = [f32::NEG_INFINITY; 3]; + for s in &specs { + for a in 0..3 { + min[a] = min[a].min(s.rest_pose.translation[a]); + max[a] = max[a].max(s.rest_pose.translation[a]); + } + } + let q = |v: f32, lo: f32, hi: f32| -> u8 { + let span = hi - lo; + if span <= f32::EPSILON { + return 0; + } + (((v - lo) / span) * 255.0).round().clamp(0.0, 255.0) as u8 + }; + + let classid = Tier { + container: (BONE_CONCEPT >> 8) as u8, + member: (BONE_CONCEPT & 0xFF) as u8, + }; + + let nodes = specs + .into_iter() + .enumerate() + .map(|(pos, spec)| { + // Family node = the node's group (parent for bones, self for groups). + let fam_group = match spec.kind { + NodeKind::Group => spec.id, + NodeKind::Bone => spec.parent.unwrap_or(spec.id), + }; + let gi = group_index.get(&fam_group).copied().unwrap_or(0) & 0x0F; + let family_node = (region_code(spec.region) << 4) | gi; + + let p = spec.rest_pose.translation; + let (heel, hip) = guid::located_heel_hip( + q(p[0], min[0], max[0]), + q(p[1], min[1], max[1]), + q(p[2], min[2], max[2]), + ); + + let mut g = Guid::ZERO; + g.set_tier(guid::TIER_CLASSID, classid); + g.set_tier(guid::TIER_HEEL, heel); + g.set_tier(guid::TIER_HIP, hip); + g.set_tier( + guid::TIER_LEAF, + Tier { + container: family_node, + member: identity_of[pos], + }, + ); + + Bone { spec, guid: g } + }) + .collect(); + Self { nodes } + } + + /// All resolved nodes (bones and grouping nodes). + #[must_use] + pub fn nodes(&self) -> &[Bone] { + &self.nodes + } + + /// Look up a node by atlas id. + #[must_use] + pub fn get(&self, id: AtlasId) -> Option<&Bone> { + self.nodes.iter().find(|b| b.id() == id) + } + + /// The direct children of `id` in the partonomy. + pub fn children_of(&self, id: AtlasId) -> impl Iterator { + self.nodes.iter().filter(move |b| b.parent() == Some(id)) + } + + /// Every clamped convergence anchor — i.e. every actual bone. This is the + /// non-negotiable rigid frame the splat-fit clamps to. + pub fn clamped_anchors(&self) -> impl Iterator { + self.nodes.iter().filter(|b| b.is_clamped_anchor()) + } + + /// `true` if `ancestor` is a partonomy ancestor of `descendant` (walking + /// the parent links). With Located addressing, partonomy is the parent-link + /// tree (the categorical family lives in the leaf byte, the spatial HHTL is + /// the orthogonal location axis), so ancestry is a link walk, not an + /// address-prefix test. + #[must_use] + pub fn is_ancestor(&self, ancestor: AtlasId, descendant: AtlasId) -> bool { + let mut cur = self.get(descendant).and_then(Bone::parent); + while let Some(p) = cur { + if p == ancestor { + return true; + } + cur = self.get(p).and_then(Bone::parent); + } + false + } +} + +// ───────────────────────────────────────────────────────────────────────── +// The curated atlas. v0 proportional T-pose (see crate-level honesty fence). +// Atlas-id allocation blocks (stable forever): +// 1 skeletal system (root) +// 10..19 region groups +// 100..199 skull group + cranial bones +// 200..299 vertebral column (200 + ordinal) +// 400..499 thoracic cage (sternum, ribs: 400 + 2*i + side) +// 1000.. appendicular, left (1000 + offset) +// 2000.. appendicular, right (2000 + offset) +// ───────────────────────────────────────────────────────────────────────── + +fn atlas() -> Vec { + let mut v: Vec = Vec::new(); + + let group = |id, fma, la, en, parent, region| BoneSpec { + id, + fma, + name_la: la, + name_en: en, + parent, + region, + kind: NodeKind::Group, + rest_pose: RigidTransform::at([0.0, 0.0, 0.0]), + }; + let bone = |id, fma, la, en, parent, region, pos: [f32; 3]| BoneSpec { + id, + fma, + name_la: la, + name_en: en, + parent, + region, + kind: NodeKind::Bone, + rest_pose: RigidTransform::at(pos), + }; + + use Region::{AppendicularLower, AppendicularUpper, Axial}; + + // Roots / groups (poses = region centroids, for sibling Morton placement). + let mut g = group( + 1, + Some(23881), + "systema skeletale", + "skeletal system", + None, + Axial, + ); + g.rest_pose = RigidTransform::at([0.0, 0.5, 0.0]); + v.push(g); + + let mut ax = group( + 10, + Some(23879), + "skeleton axiale", + "axial skeleton", + Some(1), + Axial, + ); + ax.rest_pose = RigidTransform::at([0.0, 0.55, 0.0]); + v.push(ax); + let mut au = group( + 11, + None, + "skeleton appendiculare superius", + "upper appendicular", + Some(1), + AppendicularUpper, + ); + au.rest_pose = RigidTransform::at([0.30, 0.45, 0.0]); + v.push(au); + let mut al = group( + 12, + None, + "skeleton appendiculare inferius", + "lower appendicular", + Some(1), + AppendicularLower, + ); + al.rest_pose = RigidTransform::at([0.0, -0.40, 0.0]); + v.push(al); + + // Skull group. + let mut sg = group( + 100, + Some(46565), + "cranium", + "skull (cranium)", + Some(10), + Axial, + ); + sg.rest_pose = RigidTransform::at([0.0, 0.95, 0.0]); + v.push(sg); + v.push(bone( + 101, + Some(46565), + "neurocranium", + "neurocranium", + Some(100), + Axial, + [0.0, 1.00, 0.0], + )); + v.push(bone( + 102, + Some(52748), + "mandibula", + "mandible", + Some(100), + Axial, + [0.0, 0.88, 0.04], + )); + v.push(bone( + 103, + Some(9613), + "os hyoideum", + "hyoid bone", + Some(100), + Axial, + [0.0, 0.82, 0.05], + )); + + // Vertebral column — the literal spine. C1..C7, T1..T12, L1..L5, sacrum, coccyx. + let mut vc = group( + 200, + Some(13478), + "columna vertebralis", + "vertebral column", + Some(10), + Axial, + ); + vc.rest_pose = RigidTransform::at([0.0, 0.45, -0.02]); + v.push(vc); + // Cranio-caudal stack: y descends from just below skull to the pelvis. + // (id = 200 + ordinal; ordinal 1..=26 top→bottom.) + let cervical = [ + (9915u32, "atlas (C1)"), + (9917, "axis (C2)"), + (9920, "vertebra C3"), + (9921, "vertebra C4"), + (9922, "vertebra C5"), + (9923, "vertebra C6"), + (9924, "vertebra C7"), + ]; + let mut ordinal = 1u32; + let mut y = 0.86f32; + for (fma, en) in cervical { + v.push(bone( + 200 + ordinal, + Some(fma), + "vertebra cervicalis", + en, + Some(200), + Axial, + [0.0, y, -0.01], + )); + ordinal += 1; + y -= 0.022; + } + for t in 1..=12u32 { + let en: &'static str = THORACIC[(t - 1) as usize]; + v.push(bone( + 200 + ordinal, + None, + "vertebra thoracica", + en, + Some(200), + Axial, + [0.0, y, -0.02], + )); + ordinal += 1; + y -= 0.030; + } + for l in 1..=5u32 { + let en: &'static str = LUMBAR[(l - 1) as usize]; + v.push(bone( + 200 + ordinal, + None, + "vertebra lumbalis", + en, + Some(200), + Axial, + [0.0, y, -0.025], + )); + ordinal += 1; + y -= 0.045; + } + v.push(bone( + 200 + ordinal, + Some(16202), + "os sacrum", + "sacrum", + Some(200), + Axial, + [0.0, 0.02, -0.02], + )); + ordinal += 1; + v.push(bone( + 200 + ordinal, + Some(20229), + "os coccygis", + "coccyx", + Some(200), + Axial, + [0.0, -0.04, -0.01], + )); + + // Thoracic cage — sternum + 12 rib pairs. + let mut tc = group( + 400, + Some(7480), + "cavea thoracis", + "thoracic cage", + Some(10), + Axial, + ); + tc.rest_pose = RigidTransform::at([0.0, 0.52, 0.08]); + v.push(tc); + v.push(bone( + 401, + Some(7485), + "sternum", + "sternum", + Some(400), + Axial, + [0.0, 0.55, 0.11], + )); + for i in 1..=12u32 { + // Rib pair i: descends, curves laterally + anteriorly. + let ry = 0.70 - (i as f32) * 0.028; + let rx = 0.04 + (i as f32) * 0.009; + let rz = 0.06; + v.push(bone( + 400 + 2 * i, + None, + "costa", + RIB_EN[(i - 1) as usize].0, + Some(400), + Axial, + [rx, ry, rz], + )); + v.push(bone( + 401 + 2 * i, + None, + "costa", + RIB_EN[(i - 1) as usize].1, + Some(400), + Axial, + [-rx, ry, rz], + )); + } + + // Appendicular — pectoral girdle + arm, pelvic girdle + leg, per side. + for (side_off, parent_upper, parent_lower, sx, name_side) in [ + (1000u32, 11u32, 12u32, 1.0f32, "left"), + (2000, 11, 12, -1.0, "right"), + ] { + // Upper limb. + v.push(bone( + side_off + 10, + Some(13321), + "clavicula", + leak("clavicle", name_side), + Some(parent_upper), + AppendicularUpper, + [sx * 0.08, 0.72, 0.04], + )); + v.push(bone( + side_off + 11, + Some(13394), + "scapula", + leak("scapula", name_side), + Some(parent_upper), + AppendicularUpper, + [sx * 0.12, 0.62, -0.06], + )); + v.push(bone( + side_off + 12, + Some(13303), + "humerus", + leak("humerus", name_side), + Some(parent_upper), + AppendicularUpper, + [sx * 0.18, 0.45, 0.0], + )); + v.push(bone( + side_off + 13, + Some(23463), + "radius", + leak("radius", name_side), + Some(parent_upper), + AppendicularUpper, + [sx * 0.22, 0.18, 0.02], + )); + v.push(bone( + side_off + 14, + Some(23466), + "ulna", + leak("ulna", name_side), + Some(parent_upper), + AppendicularUpper, + [sx * 0.20, 0.17, 0.0], + )); + // Lower limb. + v.push(bone( + side_off + 20, + Some(16585), + "os coxae", + leak("hip bone", name_side), + Some(parent_lower), + AppendicularLower, + [sx * 0.10, 0.0, 0.0], + )); + v.push(bone( + side_off + 21, + Some(9611), + "femur", + leak("femur", name_side), + Some(parent_lower), + AppendicularLower, + [sx * 0.10, -0.25, 0.0], + )); + v.push(bone( + side_off + 22, + Some(24485), + "patella", + leak("patella", name_side), + Some(parent_lower), + AppendicularLower, + [sx * 0.10, -0.46, 0.05], + )); + v.push(bone( + side_off + 23, + Some(12856), + "tibia", + leak("tibia", name_side), + Some(parent_lower), + AppendicularLower, + [sx * 0.09, -0.66, 0.01], + )); + v.push(bone( + side_off + 24, + Some(24479), + "fibula", + leak("fibula", name_side), + Some(parent_lower), + AppendicularLower, + [sx * 0.12, -0.66, 0.0], + )); + } + + v +} + +/// Build a `&'static str` "left/right " name. Leaks intentionally — the +/// atlas is built once per process and the resolved [`Skeleton`] is long-lived; +/// the handful of leaked names is bounded and never grows after `resolve`. +fn leak(base: &str, side: &str) -> &'static str { + Box::leak(format!("{side} {base}").into_boxed_str()) +} + +const THORACIC: [&str; 12] = [ + "vertebra T1", + "vertebra T2", + "vertebra T3", + "vertebra T4", + "vertebra T5", + "vertebra T6", + "vertebra T7", + "vertebra T8", + "vertebra T9", + "vertebra T10", + "vertebra T11", + "vertebra T12", +]; +const LUMBAR: [&str; 5] = [ + "vertebra L1", + "vertebra L2", + "vertebra L3", + "vertebra L4", + "vertebra L5", +]; +/// `(left, right)` English names for rib pairs 1..=12. +const RIB_EN: [(&str, &str); 12] = [ + ("left rib 1", "right rib 1"), + ("left rib 2", "right rib 2"), + ("left rib 3", "right rib 3"), + ("left rib 4", "right rib 4"), + ("left rib 5", "right rib 5"), + ("left rib 6", "right rib 6"), + ("left rib 7", "right rib 7"), + ("left rib 8", "right rib 8"), + ("left rib 9", "right rib 9"), + ("left rib 10", "right rib 10"), + ("left rib 11", "right rib 11"), + ("left rib 12", "right rib 12"), +]; + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + fn skel() -> Skeleton { + Skeleton::resolve() + } + + #[test] + fn bone_concept_resolves_in_codebook() { + assert_eq!(BONE_CONCEPT, 0x0A03); + assert_eq!( + ogar_vocab::canonical_concept_domain(BONE_CONCEPT), + ogar_vocab::ConceptDomain::Anatomy, + ); + } + + #[test] + fn partonomy_is_a_tree() { + let s = skel(); + let ids: HashSet = s.nodes().iter().map(Bone::id).collect(); + assert_eq!(ids.len(), s.nodes().len(), "atlas ids are unique"); + let mut roots = 0; + for b in s.nodes() { + match b.parent() { + None => roots += 1, + Some(p) => assert!(ids.contains(&p), "{} parent {p} missing", b.name_en()), + } + } + assert_eq!(roots, 1, "exactly one skeletal-system root"); + } + + fn by_name<'a>(s: &'a Skeleton, name: &str) -> &'a Bone { + s.nodes() + .iter() + .find(|b| b.name_en() == name) + .unwrap_or_else(|| panic!("{name} present")) + } + + #[test] + fn classid_tier_is_anatomy_bone() { + // Every node's GUID routes on the `bone` concept (Anatomy:bone, 0x0A03). + let s = skel(); + for b in s.nodes() { + assert_eq!(b.guid().classid(), 0x0A03, "{}", b.name_en()); + let t = b.guid().tier(guid::TIER_CLASSID); + assert_eq!((t.container, t.member), (0x0A, 0x03)); + } + } + + #[test] + fn heel_encodes_laterality() { + // Located precondition: left/right twins differ in HEEL (the coronal X + // axis = laterality), so an X-ray / ultrasound sweep can tell sides apart + // from the spatial tier — while remaining the SAME categorical family. + let s = skel(); + let left_femur = s.get(1021).unwrap(); + let right_femur = s.get(2021).unwrap(); + assert_ne!(left_femur.heel(), right_femur.heel(), "laterality in HEEL"); + assert!( + left_femur.guid().same_family(&right_femur.guid()), + "both are lower-limb-long-bone family", + ); + assert_ne!(left_femur.identity(), right_femur.identity()); + } + + #[test] + fn hip_encodes_depth() { + // HIP = the anterior-posterior depth slice: the sternum (anterior) and a + // thoracic vertebra (posterior) sit in different depth tiers. + let s = skel(); + let sternum = by_name(&s, "sternum"); + let vertebra = by_name(&s, "vertebra T6"); + let hip = |b: &Bone| b.guid().tier(guid::TIER_HIP); + assert_ne!(hip(sternum), hip(vertebra), "anterior vs posterior depth"); + } + + #[test] + fn leaf_is_family_identity_and_node_keys_unique() { + let s = skel(); + let mut keys: HashSet<[u8; morton::KEY_BYTES]> = HashSet::new(); + let mut leaves: HashSet<(u8, u8)> = HashSet::new(); + for b in s.nodes() { + assert!( + keys.insert(b.node_key()), + "node-key collision on {}", + b.name_en() + ); + assert!( + leaves.insert((b.family_node(), b.identity())), + "leaf family:identity collision on {}", + b.name_en() + ); + } + } + + #[test] + fn family_node_groups_bones_identity_attached() { + // Two vertebrae share the vertebral-column family node; their identities + // differ ("identity attached to family node"). A femur is a different + // family. The group node itself is identity 0 — it IS the family node. + let s = skel(); + let t1 = by_name(&s, "vertebra T1"); + let t2 = by_name(&s, "vertebra T2"); + let femur = s.get(1021).unwrap(); + let column = s.get(200).unwrap(); // vertebral column group + + assert!(t1.guid().same_family(&t2.guid()), "same family node"); + assert_ne!( + t1.identity(), + t2.identity(), + "identity discriminates within" + ); + assert!(!t1.guid().same_family(&femur.guid())); + + assert_eq!(column.identity(), 0, "the group node IS the family node"); + assert_eq!(column.family_node(), t1.family_node(), "its bones share it"); + assert!(t1.identity() >= 1, "bones are members (identity >= 1)"); + } + + #[test] + fn every_bone_is_a_clamped_anchor() { + // The operator's "bones not negotiable" as an invariant: no un-clamped bone. + let s = skel(); + let anchors = s.clamped_anchors().count(); + assert!( + anchors >= 60, + "expected a meticulous skeleton, got {anchors} anchors" + ); + for b in s.nodes() { + if matches!(b.kind(), NodeKind::Bone) { + assert!( + b.is_clamped_anchor(), + "{} is a bone but not clamped", + b.name_en() + ); + } + } + } + + #[test] + fn rest_frames_are_valid_rigid_transforms() { + for b in skel().nodes() { + assert!( + b.rest_pose().is_valid(), + "{} has a non-unit rotation", + b.name_en() + ); + } + } + + #[test] + fn partonomy_ancestry_walks_parent_links() { + let s = skel(); + let sacrum = by_name(&s, "sacrum").id(); + assert!(s.is_ancestor(200, sacrum), "vertebral column ⊃ sacrum"); + assert!( + s.is_ancestor(1, sacrum), + "skeletal system ⊃ sacrum (transitive)" + ); + assert!( + !s.is_ancestor(200, 1021), + "column is not an ancestor of the femur" + ); + } + + #[test] + fn classid_stability_snapshot() { + // Pin the classid tier: every bone routes on Anatomy:bone. A change that + // moves this breaks the immutability the splat-fit relies on. + let s = skel(); + for b in s.clamped_anchors() { + assert_eq!(b.guid().classid(), 0x0A03); + } + } +} diff --git a/crates/ogar-fma-skeleton/src/morton.rs b/crates/ogar-fma-skeleton/src/morton.rs new file mode 100644 index 0000000..a27b48d --- /dev/null +++ b/crates/ogar-fma-skeleton/src/morton.rs @@ -0,0 +1,434 @@ +//! The **4×4 Morton-tile pyramid** — the uniform family-node address cascade. +//! +//! # Why this shape (operator correction, 2026-06-23) +//! +//! The canonical node key is **16 bytes = 16×8-bit family nodes**, a *uniform* +//! array — never a heterogeneous `12+4` carve. Uniformity is load-bearing: +//! it is exactly why the operator reversed the `4/3/3` v8-native carve on +//! 2026-06-10 ("broke the uniform Morton stride"). Each **nibble** (4 bits) is +//! one **4×4 Morton tile**: 2 bits X interleaved with 2 bits Y. 16 bytes ⇒ 32 +//! nibble-levels ⇒ a 32-level pyramid of 4×4 spatial refinements, uniform +//! stride throughout. +//! +//! ```text +//! byte: 0 1 2 3 ... 15 +//! ┌────┬────┬────┬────┬─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┬────┐ +//! │ hi lo │ … 16 family-node bytes = 16×8 bit = 128-bit key │ +//! └────┴────┴────┴────┴─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┴────┘ +//! nibble: 0 1 2 3 … 31 +//! each nibble = one 4×4 Morton tile: bit0=x0 bit1=y0 bit2=x1 bit3=y1 +//! ``` +//! +//! # The perturbation-shader cascade (why we can't drop the Morton structure) +//! +//! Per the OGAR canon (`CLAUDE.md` §"Perturbation encoding"), the stacked +//! perturbation decomposes as `(exponent, location, phase, magnitude)`. Three +//! of the four live in the address itself: **exponent = the pyramid level** +//! (`tier = level >> 2`), **location = the Morton tile at that level**, and +//! **phase = a deterministic recurrence from the address** (generated, never +//! stored). Only **magnitude** is stored — and it lives in the splat layer +//! (ndarray), not here. If the address stored opaque tree-branch indices +//! instead of Morton tiles, `exponent` + `location` would be lost and the +//! shader pyramid would have nothing to refine — "the data we save we lose." +//! OGAR owns `(exponent, location)`; ndarray owns `(phase, magnitude)`. +//! +//! # The one-address collapse (D-BOTHCASC) +//! +//! Because each nibble is a 4×4 Morton tile of a child's position within its +//! parent's bounding box, **nibble-prefix containment is simultaneously +//! spatial containment and partonomy containment**. A bone's address is built +//! by descending the Morton quadtree of its siblings ([`assign_morton_suffixes`]), +//! so `parent.is_ancestor_of(child)` holds by construction *and* spatially +//! near siblings share a longer Morton prefix. + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Bits per nibble-level (one 4×4 Morton tile). +pub const TILE_BITS: u32 = 4; +/// Bits per axis per level (2 ⇒ 4 positions per axis ⇒ a 4×4 tile). +pub const AXIS_BITS: u32 = 2; +/// Positions per axis per level (`1 << AXIS_BITS` = 4). +pub const AXIS_CELLS: u8 = 1 << AXIS_BITS; +/// Bytes in a canonical family-node key (16 × 8 bit = 128 bit). +pub const KEY_BYTES: usize = 16; +/// Nibble-levels in the full pyramid (`KEY_BYTES * 2` = 32). +pub const LEVELS: usize = KEY_BYTES * 2; + +/// Encode a single 4×4 Morton tile from 2-bit axis coordinates. +/// +/// `x` and `y` are each in `0..4`; the result is the interleaved nibble +/// `0..16` with `bit0 = x0, bit1 = y0, bit2 = x1, bit3 = y1` (standard +/// Z-order, X in the even bit-positions). +/// +/// ``` +/// use ogar_fma_skeleton::morton::tile; +/// assert_eq!(tile(0, 0), 0b0000); +/// assert_eq!(tile(3, 0), 0b0101); // x=11, y=00 -> bits 0 and 2 set +/// assert_eq!(tile(0, 3), 0b1010); // x=00, y=11 -> bits 1 and 3 set +/// assert_eq!(tile(3, 3), 0b1111); +/// ``` +#[must_use] +pub const fn tile(x: u8, y: u8) -> u8 { + let x = x & 0b11; + let y = y & 0b11; + (x & 1) | ((y & 1) << 1) | ((x & 2) << 1) | ((y & 2) << 2) +} + +/// Decode a 4×4 Morton tile nibble back into `(x, y)` 2-bit axis coordinates. +/// +/// Inverse of [`tile`]. The result components are each in `0..4`. +/// +/// ``` +/// use ogar_fma_skeleton::morton::{tile, untile}; +/// for x in 0u8..4 { +/// for y in 0u8..4 { +/// assert_eq!(untile(tile(x, y)), (x, y)); +/// } +/// } +/// ``` +#[must_use] +pub const fn untile(nibble: u8) -> (u8, u8) { + let n = nibble & 0b1111; + let x = (n & 0b0001) | ((n >> 1) & 0b0010); + let y = ((n >> 1) & 0b0001) | ((n >> 2) & 0b0010); + (x, y) +} + +/// The cascade **tier** a pyramid level belongs to — `level >> 2`, a shift, +/// never a branch (the canon's uniform-stride property). Four nibble-levels +/// per tier ⇒ a 256×256 centroid tile per tier (`CLAUDE.md` §"Tier +/// interpretation"). +#[must_use] +pub const fn tier(level: usize) -> usize { + level >> 2 +} + +/// A canonical **family-node address**: 16 bytes (16×8 bit) of Morton-tile +/// pyramid, plus the number of assigned nibble-levels (`depth`). Unassigned +/// tail levels are zero (the canon's RESERVE-DON'T-RECLAIM / zero-fallback +/// ladder: zero = "not yet consulted", never compacted away). +/// +/// `depth` is in **nibble-levels** (`0..=32`); each level is one 4×4 tile. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct FamilyAddress { + tiles: [u8; KEY_BYTES], + depth: u8, +} + +impl Default for FamilyAddress { + fn default() -> Self { + Self::ROOT + } +} + +impl FamilyAddress { + /// The empty (depth-0) root address — the whole-key zero prefix. + pub const ROOT: Self = Self { + tiles: [0u8; KEY_BYTES], + depth: 0, + }; + + /// Build an address from an explicit nibble-level path (each entry a + /// 4×4 Morton tile `0..16`). Levels beyond [`LEVELS`] are ignored; tile + /// values are masked to 4 bits. + #[must_use] + pub fn from_nibbles(nibbles: &[u8]) -> Self { + let mut addr = Self::ROOT; + for &n in nibbles { + addr.push(n); + } + addr + } + + /// Number of assigned nibble-levels (`0..=32`). + #[must_use] + pub const fn depth(&self) -> u8 { + self.depth + } + + /// The raw 16-byte key (Morton tiles packed two-per-byte, hi nibble first). + #[must_use] + pub const fn bytes(&self) -> [u8; KEY_BYTES] { + self.tiles + } + + /// The Morton tile at nibble-level `level` (`0` = coarsest). Returns `0` + /// for levels at or beyond [`LEVELS`]. + #[must_use] + pub const fn nibble(&self, level: usize) -> u8 { + if level >= LEVELS { + return 0; + } + let byte = self.tiles[level >> 1]; + if level & 1 == 0 { + byte >> 4 + } else { + byte & 0x0F + } + } + + /// Append one 4×4 Morton tile at the current depth. No-op once the + /// pyramid is full ([`LEVELS`] levels). The tile is masked to 4 bits. + pub fn push(&mut self, t: u8) { + let level = self.depth as usize; + if level >= LEVELS { + return; + } + let t = t & 0x0F; + let byte = level >> 1; + if level & 1 == 0 { + self.tiles[byte] = (self.tiles[byte] & 0x0F) | (t << 4); + } else { + self.tiles[byte] = (self.tiles[byte] & 0xF0) | t; + } + self.depth += 1; + } + + /// Builder form of [`push`](Self::push). + #[must_use] + pub fn with(mut self, t: u8) -> Self { + self.push(t); + self + } + + /// Extend this address by a whole Morton-tile suffix. + #[must_use] + pub fn extend(mut self, suffix: &[u8]) -> Self { + for &t in suffix { + self.push(t); + } + self + } + + /// Number of leading nibble-levels shared with `other`, capped at the + /// shallower of the two depths. + #[must_use] + pub fn common_prefix_len(&self, other: &Self) -> u8 { + let limit = self.depth.min(other.depth) as usize; + let mut n = 0u8; + while (n as usize) < limit && self.nibble(n as usize) == other.nibble(n as usize) { + n += 1; + } + n + } + + /// `true` if `self` is a partonomy/spatial **ancestor** of `other`: every + /// assigned nibble of `self` is a prefix of `other`. A node is its own + /// ancestor (reflexive). This is the canon's `is_ancestor_of` = + /// prefix-containment, which — because each nibble is a Morton tile — is + /// simultaneously spatial containment. + #[must_use] + pub fn is_ancestor_of(&self, other: &Self) -> bool { + self.depth <= other.depth && self.common_prefix_len(other) == self.depth + } +} + +impl core::fmt::Debug for FamilyAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "FamilyAddress(")?; + for level in 0..self.depth as usize { + write!(f, "{:x}", self.nibble(level))?; + } + write!(f, "/d{})", self.depth) + } +} + +/// The axis-aligned bounding box a Morton subdivision refines within. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Bounds { + /// Minimum X (e.g. anatomical left→right) and Y (superior→inferior). + pub min: (f32, f32), + /// Maximum X and Y. + pub max: (f32, f32), +} + +impl Bounds { + /// The tightest box covering all `points` (each `(x, y)`). Degenerate + /// (zero-width) axes are tolerated — [`tile_of`](Self::tile_of) clamps. + #[must_use] + pub fn of(points: &[(f32, f32)]) -> Self { + let mut min = (f32::INFINITY, f32::INFINITY); + let mut max = (f32::NEG_INFINITY, f32::NEG_INFINITY); + for &(x, y) in points { + min.0 = min.0.min(x); + min.1 = min.1.min(y); + max.0 = max.0.max(x); + max.1 = max.1.max(y); + } + if !min.0.is_finite() { + min = (0.0, 0.0); + max = (0.0, 0.0); + } + Self { min, max } + } + + /// The 4×4 Morton tile a point falls into within this box. + #[must_use] + pub fn tile_of(&self, p: (f32, f32)) -> u8 { + tile( + self.axis_cell(p.0, self.min.0, self.max.0), + self.axis_cell(p.1, self.min.1, self.max.1), + ) + } + + /// The sub-box of one 4×4 cell (`tx, ty` each `0..4`). + #[must_use] + pub fn subdivide(&self, t: u8) -> Self { + let (tx, ty) = untile(t); + let wx = (self.max.0 - self.min.0) / f32::from(AXIS_CELLS); + let wy = (self.max.1 - self.min.1) / f32::from(AXIS_CELLS); + let minx = self.min.0 + f32::from(tx) * wx; + let miny = self.min.1 + f32::from(ty) * wy; + Self { + min: (minx, miny), + max: (minx + wx, miny + wy), + } + } + + fn axis_cell(&self, v: f32, lo: f32, hi: f32) -> u8 { + let span = hi - lo; + if span <= f32::EPSILON { + return 0; + } + let frac = ((v - lo) / span) * f32::from(AXIS_CELLS); + let idx = frac.floor() as i32; + idx.clamp(0, i32::from(AXIS_CELLS) - 1) as u8 + } +} + +/// Maximum Morton-tile suffix length assigned to separate one group of +/// siblings. A cap (not a limit reached in practice for the curated atlas); +/// if siblings still collide at this depth the caller's uniqueness test +/// fails, signalling two bones with coincident rest-pose centroids to fix. +pub const MAX_SUFFIX_LEVELS: usize = 8; + +/// Assign each sibling a Morton-tile suffix by descending the 4×4 quadtree +/// of their `centroids` until every sibling occupies a distinct cell. +/// +/// Returned `suffixes[i]` is the nibble path for `centroids[i]`, refining its +/// parent's address. Because all suffixes descend the *same* quadtree, the +/// shared prefix of two siblings is their spatial proximity — the Morton +/// locality property. Siblings in the same coarse cell recurse deeper; lone +/// siblings stop early (variable-length suffixes = the pyramid adapting to +/// where children cluster). +/// +/// A single child (or none) gets an empty suffix — there is nothing to +/// disambiguate. +#[must_use] +pub fn assign_morton_suffixes(centroids: &[(f32, f32)]) -> Vec> { + let mut out = vec![Vec::new(); centroids.len()]; + if centroids.len() > 1 { + let all: Vec = (0..centroids.len()).collect(); + refine(centroids, &all, Bounds::of(centroids), 0, &mut out); + } + out +} + +fn refine( + centroids: &[(f32, f32)], + idxs: &[usize], + bounds: Bounds, + level: usize, + out: &mut [Vec], +) { + if idxs.len() <= 1 || level >= MAX_SUFFIX_LEVELS { + return; + } + // Bucket the siblings by their 4×4 tile in this box. + let mut buckets: [Vec; 16] = Default::default(); + for &i in idxs { + buckets[bounds.tile_of(centroids[i]) as usize].push(i); + } + for (t, members) in buckets.into_iter().enumerate() { + if members.is_empty() { + continue; + } + let t = t as u8; + for &i in &members { + out[i].push(t); + } + if members.len() > 1 { + refine(centroids, &members, bounds.subdivide(t), level + 1, out); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tile_roundtrips_all_16() { + for n in 0u8..16 { + let (x, y) = untile(n); + assert!(x < 4 && y < 4); + assert_eq!(tile(x, y), n); + } + } + + #[test] + fn tier_is_level_shift_two() { + assert_eq!(tier(0), 0); + assert_eq!(tier(3), 0); + assert_eq!(tier(4), 1); + assert_eq!(tier(11), 2); // HEEL/HIP/TWIG boundary: level 11 -> tier 2 (TWIG) + } + + #[test] + fn push_and_nibble_are_inverse() { + let path = [0x3u8, 0xA, 0x0, 0xF, 0x5, 0x9]; + let a = FamilyAddress::from_nibbles(&path); + assert_eq!(a.depth(), path.len() as u8); + for (level, &n) in path.iter().enumerate() { + assert_eq!(a.nibble(level), n); + } + } + + #[test] + fn ancestor_is_prefix_containment() { + let parent = FamilyAddress::from_nibbles(&[0x1, 0x2]); + let child = FamilyAddress::from_nibbles(&[0x1, 0x2, 0x9, 0x4]); + let cousin = FamilyAddress::from_nibbles(&[0x1, 0x3]); + assert!(parent.is_ancestor_of(&child)); + assert!(parent.is_ancestor_of(&parent)); // reflexive + assert!(!child.is_ancestor_of(&parent)); // deeper is not ancestor + assert!(!cousin.is_ancestor_of(&child)); + } + + #[test] + fn uniform_16_byte_key_width() { + assert_eq!(LEVELS, 32); + assert_eq!(KEY_BYTES, 16); + let full = FamilyAddress::from_nibbles(&[0xF; LEVELS]); + assert_eq!(full.depth(), 32); + assert_eq!(full.bytes(), [0xFF; KEY_BYTES]); + } + + #[test] + fn morton_suffixes_are_distinct_and_local() { + // Four points in four quadrants -> one-nibble distinct suffixes. + let pts = [(0.0f32, 0.0f32), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)]; + let s = assign_morton_suffixes(&pts); + assert_eq!(s.len(), 4); + let set: std::collections::HashSet<_> = s.iter().collect(); + assert_eq!(set.len(), 4, "all suffixes distinct"); + for suf in &s { + assert_eq!(suf.len(), 1, "one level separates four quadrants"); + } + } + + #[test] + fn colinear_siblings_descend_deeper() { + // 8 colinear points along Y must still separate (vertebral-column case). + let pts: Vec<(f32, f32)> = (0..8).map(|i| (0.0, i as f32)).collect(); + let s = assign_morton_suffixes(&pts); + let set: std::collections::HashSet<_> = s.iter().collect(); + assert_eq!( + set.len(), + 8, + "colinear siblings all separate via deeper Morton levels" + ); + } +} diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 082cd69..ca3987e 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1069,9 +1069,20 @@ impl Class { /// 0x07XX reserved: OSINT /// 0x08XX reserved: OCR /// 0x09XX reserved: Health -/// 0x0AXX+ unassigned +/// 0x0AXX Anatomy (FMA reference ontology; bones/skeleton) +/// 0x0CXX+ unassigned /// ``` /// +/// **Anatomy vs Health (the firewall split).** `0x0AXX` Anatomy is the +/// **public reference structure** (the femur exists; it is `part_of` the +/// lower limb) — the FMA atlas frame every imaging modality registers +/// against. It is deliberately NOT in `0x09XX` Health: a clinical *finding +/// about* anatomy (a fracture diagnosis on a named patient) is Health PHI, +/// but the anatomical *structure itself* is public and must not be pulled +/// into medcare-rs's fail-closed Health RBAC coverage set. This is why the +/// earlier "FMA / SNOMED converges into Health" forward-note (below) lands +/// in its own domain instead: reference ≠ PHI. +/// /// Reserved blocks have a placeholder [`ConceptDomain`] variant so a /// consumer routing on `id >> 8` returns a stable domain tag even before /// any concept lands in that block. @@ -1159,6 +1170,20 @@ const CODEBOOK: &[(&str, u16)] = &[ ("treatment", 0x0905), ("visit", 0x0906), ("vital_sign", 0x0907), + // ── 0x0AXX — Anatomy domain (FMA reference ontology) ── + // The public anatomical reference frame consumed by the splat-native + // ultrasound arc (`docs/SPLAT-NATIVE-CUSTOMER.md` §6 litmus) and the FMA + // skeletal spine (`crates/ogar-fma-skeleton`). These are the *kinds* + // (Bone, Skeleton, …); the ~206 individual bones are NOT concept slots — + // they live as cascade-path nodes (FMA partonomy → HEEL/HIP/TWIG prefix + // tree), the same way Wikidata-HHTL lives in the path, not the codebook. + // `bone` is the clamped convergence-anchor class: bones are the rigid, + // non-negotiable frame every imaging modality (ViT / X-ray / ultrasound + // × Doppler) registers against. See `docs/FMA-SKELETON-CONVERGENCE-ANCHOR.md`. + ("anatomical_structure", 0x0A01), + ("skeleton", 0x0A02), + ("bone", 0x0A03), + ("joint", 0x0A04), // ── 0x0BXX — Auth domain (IAM; provider-agnostic — the AuthStore class family) ── // Per `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` §7 + `APP-CLASS-CODEBOOK-LAYOUT.md` // §2: auth is a CORE domain of its own (`0x0B`), cross-app and @@ -1203,6 +1228,12 @@ pub enum ConceptDomain { Ocr, /// `0x09XX` — Health (clinical / patient / care). Health, + /// `0x0AXX` — Anatomy (FMA reference ontology; the public anatomical + /// structure frame — bones / skeleton / joints). Distinct from + /// [`Health`](Self::Health): reference structure is public, a clinical + /// finding *about* it is PHI. The clamped convergence-anchor frame for + /// the splat-native imaging arc. + Anatomy, /// `0x0BXX` — Auth (IAM; provider-agnostic — the AuthStore class /// family: `auth_store` + per-IdP profiles `auth_zitadel` / /// `auth_zanzibar` / `auth_ory_keto`). See @@ -1224,6 +1255,7 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain { 0x07 => ConceptDomain::Osint, 0x08 => ConceptDomain::Ocr, 0x09 => ConceptDomain::Health, + 0x0A => ConceptDomain::Anatomy, 0x0B => ConceptDomain::Auth, _ => ConceptDomain::Unassigned, } @@ -1488,6 +1520,25 @@ pub mod class_ids { /// `Healthcare:VitalSign`. pub const VITAL_SIGN: u16 = 0x0907; + // ── 0x0AXX — Anatomy domain (FMA reference ontology) ── + + /// `anatomical_structure` (`0x0A01`) — FMA's universal root kind + /// (everything in the atlas `is-a` this). The abstract anchor of the + /// anatomy partonomy. + pub const ANATOMICAL_STRUCTURE: u16 = 0x0A01; + /// `skeleton` (`0x0A02`) — the whole-body skeletal system; the root of + /// the bone partonomy (`crates/ogar-fma-skeleton`). + pub const SKELETON: u16 = 0x0A02; + /// `bone` (`0x0A03`) — a skeletal element. **The clamped convergence + /// anchor**: bones are the rigid, non-negotiable reference frame the + /// splat-fit registers against (`docs/FMA-SKELETON-CONVERGENCE-ANCHOR.md`). + /// The ~206 individual bones are cascade-path nodes under this concept, + /// not separate codebook slots. + pub const BONE: u16 = 0x0A03; + /// `joint` (`0x0A04`) — an articulation between bones (the skeletal + /// graph's edges, when materialized). + pub const JOINT: u16 = 0x0A04; + // ── 0x0BXX — Auth domain (IAM; the AuthStore class family) ── /// `auth_store` (`0x0B01`) — the IdP→classid mapping base class. Does @@ -1554,6 +1605,11 @@ pub mod class_ids { ("treatment", TREATMENT), ("visit", VISIT), ("vital_sign", VITAL_SIGN), + // 0x0AXX — anatomy (FMA reference ontology) + ("anatomical_structure", ANATOMICAL_STRUCTURE), + ("skeleton", SKELETON), + ("bone", BONE), + ("joint", JOINT), // 0x0BXX — auth (AuthStore class family) ("auth_store", AUTH_STORE), ("auth_zitadel", AUTH_ZITADEL), @@ -1564,7 +1620,7 @@ pub mod class_ids { #[cfg(test)] mod tests { use super::*; - use crate::{canonical_concept_id, CODEBOOK}; + use crate::{CODEBOOK, canonical_concept_id}; #[test] fn constants_match_codebook() { @@ -2408,6 +2464,11 @@ pub fn all_promoted_classes() -> Vec { treatment(), visit(), vital_sign(), + // 0x0AXX — anatomy arm (FMA reference kinds), in class_ids::ALL order. + anatomical_structure(), + skeleton(), + bone(), + joint(), // 0x0BXX — auth arm (the AuthStore class family, keystone §7), // in class_ids::ALL order. auth_store(), @@ -3549,6 +3610,79 @@ pub fn auth_ory_keto() -> Class { auth_provider("AuthOryKeto", "auth_ory_keto") } +// ── 0x0AXX — Anatomy domain builders (FMA reference kinds) ── +// +// The public anatomical reference frame consumed by the splat-native arc +// (`docs/SPLAT-NATIVE-CUSTOMER.md`) and the FMA skeletal spine +// (`crates/ogar-fma-skeleton`). These are the *kinds* (the FMA universal +// root, the skeletal system, the bone, the joint) — the ~206 individual +// bones are NOT concept slots; they are cascade-path nodes whose 16×8-bit +// Morton-tile address places them in the partonomy + body volume. See +// `docs/FMA-SKELETON-CONVERGENCE-ANCHOR.md`. + +/// The `anatomical_structure` (`0x0A01`) — FMA's universal root kind +/// (everything in the atlas `is-a` this). The abstract anchor of the +/// anatomy partonomy. +#[must_use] +pub fn anatomical_structure() -> Class { + let mut c = Class::new("AnatomicalStructure"); + c.language = Language::Unknown; + c.canonical_concept = Some("anatomical_structure".to_string()); + let mut fma_id = Attribute::new("fma_id"); + fma_id.type_name = Some("string".to_string()); + let mut name_la = Attribute::new("name_la"); // Terminologia Anatomica + name_la.type_name = Some("string".to_string()); + c.attributes = vec![fma_id, name_la]; + c +} + +/// The `skeleton` (`0x0A02`) — the whole-body skeletal system; the root of +/// the bone partonomy (`crates/ogar-fma-skeleton`). +#[must_use] +pub fn skeleton() -> Class { + let mut c = Class::new("Skeleton"); + c.language = Language::Unknown; + c.canonical_concept = Some("skeleton".to_string()); + c.parent = Some("AnatomicalStructure".to_string()); + c.associations = vec![family_edge("bones", "Bone")]; + c +} + +/// The `bone` (`0x0A03`) — a skeletal element. **The clamped convergence +/// anchor**: the rigid, non-negotiable frame the splat-fit registers +/// against. The ~206 individual bones are cascade-path nodes under this +/// concept (FMA partonomy → 16×8-bit Morton-tile address), not codebook +/// slots. See `docs/FMA-SKELETON-CONVERGENCE-ANCHOR.md`. +#[must_use] +pub fn bone() -> Class { + let mut c = Class::new("Bone"); + c.language = Language::Unknown; + c.canonical_concept = Some("bone".to_string()); + c.parent = Some("AnatomicalStructure".to_string()); + c.associations = vec![ + family_edge("part_of", "Skeleton"), + family_edge("articulates", "Joint"), + ]; + let mut rest_pose = Attribute::new("rest_pose"); // rigid transform (T-pose) + rest_pose.type_name = Some("string".to_string()); + let mut clamped = Attribute::new("clamped"); // bones are always anchors + clamped.type_name = Some("boolean".to_string()); + c.attributes = vec![rest_pose, clamped]; + c +} + +/// The `joint` (`0x0A04`) — an articulation between bones (the skeletal +/// graph's edges, when materialized). +#[must_use] +pub fn joint() -> Class { + let mut c = Class::new("Joint"); + c.language = Language::Unknown; + c.canonical_concept = Some("joint".to_string()); + c.parent = Some("AnatomicalStructure".to_string()); + c.associations = vec![family_edge("connects", "Bone")]; + c +} + #[cfg(test)] mod tests { use super::*; @@ -3702,10 +3836,11 @@ mod tests { fn tax_policy_is_an_erp_boundary_edge_not_in_project_evidence() { // TaxPolicy is a family edge on the canonical shape ... let bwe = billable_work_entry(); - assert!(bwe - .associations - .iter() - .any(|e| e.class_name.as_deref() == Some("TaxPolicy"))); + assert!( + bwe.associations + .iter() + .any(|e| e.class_name.as_deref() == Some("TaxPolicy")) + ); // ... but the project curator records work evidence with no tax. let mut op = Class::new("TimeEntry"); op.source_domain = Some("project".to_string()); @@ -4160,10 +4295,12 @@ mod tests { assert_eq!(canonical_concept_domain(0x0900), ConceptDomain::Health); assert_eq!(canonical_concept_domain(0x0B00), ConceptDomain::Auth); assert_eq!(canonical_concept_domain(0x0B04), ConceptDomain::Auth); - // Unassigned blocks (3-6, A, C+). + // Anatomy block (0x0A) — FMA reference kinds. + assert_eq!(canonical_concept_domain(0x0A00), ConceptDomain::Anatomy); + assert_eq!(canonical_concept_domain(0x0A03), ConceptDomain::Anatomy); + // Unassigned blocks (3-6, C+). assert_eq!(canonical_concept_domain(0x0300), ConceptDomain::Unassigned); assert_eq!(canonical_concept_domain(0x0600), ConceptDomain::Unassigned); - assert_eq!(canonical_concept_domain(0x0A00), ConceptDomain::Unassigned); assert_eq!(canonical_concept_domain(0x0C00), ConceptDomain::Unassigned); assert_eq!(canonical_concept_domain(0xFFFF), ConceptDomain::Unassigned); } @@ -4645,7 +4782,7 @@ mod tests { assert!(!is_cross_domain_concept("project_role")); let id = canonical_concept_id("billable_work_entry").unwrap(); assert_eq!(canonical_concept_domain(id), ProjectMgmt); // home domain - // Project curator (home domain) — kept. + // Project curator (home domain) — kept. assert_eq!( canonical_concept_in_domain("TimeEntry", Some(ProjectMgmt)), "billable_work_entry" @@ -4688,10 +4825,11 @@ mod tests { .any(|a| a.name == "document" && a.class_name.as_deref() == Some("CommercialDocument")) ); - assert!(line - .associations - .iter() - .any(|a| a.name == "tax" && a.class_name.as_deref() == Some("TaxPolicy"))); + assert!( + line.associations + .iter() + .any(|a| a.name == "tax" && a.class_name.as_deref() == Some("TaxPolicy")) + ); let doc = commercial_document(); let line_items = doc @@ -4701,20 +4839,23 @@ mod tests { .unwrap(); assert_eq!(line_items.kind, AssociationKind::HasMany); assert_eq!(line_items.class_name.as_deref(), Some("CommercialLineItem")); - assert!(doc - .associations - .iter() - .any(|a| a.name == "party" && a.class_name.as_deref() == Some("BillingParty"))); - assert!(doc - .associations - .iter() - .any(|a| a.name == "currency" && a.class_name.as_deref() == Some("CurrencyPolicy"))); + assert!( + doc.associations + .iter() + .any(|a| a.name == "party" && a.class_name.as_deref() == Some("BillingParty")) + ); + assert!( + doc.associations + .iter() + .any(|a| a.name == "currency" && a.class_name.as_deref() == Some("CurrencyPolicy")) + ); let pay = payment_record(); - assert!(pay - .associations - .iter() - .any(|a| a.name == "party" && a.class_name.as_deref() == Some("BillingParty"))); + assert!( + pay.associations + .iter() + .any(|a| a.name == "party" && a.class_name.as_deref() == Some("BillingParty")) + ); assert!( pay.associations .iter() diff --git a/crates/ogar-vocab/src/ports.rs b/crates/ogar-vocab/src/ports.rs index 440337e..89dd0e6 100644 --- a/crates/ogar-vocab/src/ports.rs +++ b/crates/ogar-vocab/src/ports.rs @@ -528,7 +528,7 @@ mod tests { #[test] fn healthcare_entities_resolve_into_the_health_domain() { - use crate::{canonical_concept_domain, ConceptDomain}; + use crate::{ConceptDomain, canonical_concept_domain}; for &(name, _) in HealthcarePort::aliases() { let id = HealthcarePort::class_id(name).unwrap_or_else(|| panic!("`{name}` must resolve")); @@ -951,7 +951,7 @@ mod tests { #[test] fn odoo_commerce_models_resolve_into_the_commerce_domain() { - use crate::{canonical_concept_domain, ConceptDomain}; + use crate::{ConceptDomain, canonical_concept_domain}; // Every commerce-arm alias lands in the Commerce (0x02XX) domain. // `account.analytic.line` is the deliberate exception — it's the // cross-arm bridge into the project domain (asserted separately). diff --git a/docs/DISCOVERY-MAP.md b/docs/DISCOVERY-MAP.md index 8b0e50b..88eb94a 100644 --- a/docs/DISCOVERY-MAP.md +++ b/docs/DISCOVERY-MAP.md @@ -127,6 +127,7 @@ two halves of a cell. ADR‑026 names the cascade that ties them. | D‑CASCADE | 64→256→1024→4096→16k→64k→256k = immaterialized Morton enumeration; every level = +1 nibble. **Algorithmically grounded** by generalized Morton/Hilbert ordering for **non‑power‑of‑2 + 3D** dims (arXiv 2309.15199, Walker — the `6×4×4` example = `6 roles × 4×4 tile`; the octant recursion = the cascade; non‑pow‑2 generalization is what lets **Base17 (D‑BGZ17)** be Morton‑ordered). | G | EPIPHANY | SYN §7.5 + §7 papers | D‑MORTON | | D‑IMMAT | the cascade is a **coordinate transform, not a stored grid** (`(lat,lon)→quadkey` cheap) | G | EPIPHANY | SYN §7.5 | D‑CASCADE | | D‑NEIGH | neighbor‑XOR walk + parent‑prefix = structured‑sparse stencil (block‑banded, not sparse GEMM) | H | EPIPHANY | SYN §6 | D‑MORTON, `[per rt]` blasgraph | +| D‑FMA‑SKELETON | FMA skeleton = the **clamped convergence anchor**: ~206 bones as immutable **16×8‑bit Morton‑tile family‑node** addresses derived from rest‑pose centroids ⟹ prefix = partonomy = spatial containment (D‑BOTHCASC realized); bones are non‑negotiable Dirichlet anchors, the cross‑modal frame ViT / X‑ray / ultrasound × Doppler register onto. Address structure CODED; splat‑fit convergence CONJECTURE. | G (structure) / H (convergence) | CODED | `crates/ogar-fma-skeleton` + `docs/FMA-SKELETON-CONVERGENCE-ANCHOR.md` | D‑MORTON; `SPLAT-NATIVE-CUSTOMER.md` §6 | ### 2.2 Selection & bounds diff --git a/docs/FMA-SKELETON-CONVERGENCE-ANCHOR.md b/docs/FMA-SKELETON-CONVERGENCE-ANCHOR.md new file mode 100644 index 0000000..e05a1d9 --- /dev/null +++ b/docs/FMA-SKELETON-CONVERGENCE-ANCHOR.md @@ -0,0 +1,213 @@ +# FMA Skeleton — the Clamped Convergence Anchor + +> **Status:** CODED (crate + tests + CI) for the address/atlas structure; +> CONJECTURE for the splat-fit convergence claim (gated on the splat-native arc). +> **Authored:** 2026-06-23 (operator directive session). +> **Home crate:** `crates/ogar-fma-skeleton`. +> **Codebook:** `ogar_vocab::class_ids::{ANATOMICAL_STRUCTURE, SKELETON, BONE, JOINT}` +> (`0x0A01..0x0A04`, the new `ConceptDomain::Anatomy`). +> **Consumes:** `D-MORTON` (DISCOVERY-MAP §2.1 — nibble = one 4×4 Morton tile). +> **Customer:** `docs/SPLAT-NATIVE-CUSTOMER.md` §6 FMA litmus. + +--- + +## 0. The operator directive (2026-06-23) + +> *"FMA is a must. It must be meticulously optimized in order to later have +> stability of the human body — bones not being negotiable. The body is +> hardcoded, hand-optimized convergence optimization. Secondly, we want to +> project ViT or X-ray and especially ultrasound × Doppler."* + +Two requirements that are **one constraint**: + +> Bones must be hardcoded and stable **because** they are the cross-modal +> registration frame that ViT, X-ray, and ultrasound × Doppler all project +> onto. Request 1 (bones stable) is the *precondition* for Request 2 +> (cross-modal projection). + +The skeleton is the only rigid body in the human. Soft tissue deforms and +breathes; Doppler is velocity, not structure. So the skeleton is **not data +being fit** — it is the **boundary condition of the fit**: a hand-curated, +immutable, rigid frame. Clamping the splat-fit to it makes the optimization +well-posed and *deterministic* — the Class IIa SaMD requirement. + +--- + +## 1. Bones as clamped Dirichlet anchors + +In 3D-Gaussian-Splatting-style fitting, free Gaussians drift; the optimization +is ill-posed without anchors. The FMA skeleton supplies them: + +- **Bone Gaussians are clamped** — zero free parameters (Dirichlet boundary). +- **Soft-tissue / Doppler splats are deformable children** registered to a bone + parent (linear-blend-skinning / as-rigid-as-possible against the skeleton). +- The fit converges stably because the rigid frame is non-negotiable. + +In code this is an invariant, not a flag: `Bone::is_clamped_anchor()` is `true` +for **every** `NodeKind::Bone`. There is no un-clamped bone. The +`every_bone_is_a_clamped_anchor` test enforces it. + +--- + +## 2. The address — 16×8-bit Morton-tile family nodes (operator correction) + +The original plan modelled the cascade path as opaque tree-branch nibbles. That +**throws away the spatial structure** — "the data we save we will lose by not +adhering to the 2bit×2bit 4×4 Morton tile pyramid perturbation shader cascade." +Corrected: + +- The key is a **uniform 16-byte (16×8-bit) family-node array** — never a + heterogeneous `12+4` carve. Uniformity is load-bearing: it is exactly why the + operator reversed the `4/3/3` v8-native carve (CLAUDE.md §"3×4 PATH" — + "broke the uniform Morton stride"). +- **Each nibble is one 4×4 Morton tile**: 2 bits X interleaved with 2 bits Y. + 16 bytes ⇒ 32 nibble-levels ⇒ a 32-level pyramid of 4×4 spatial refinements, + uniform stride (`tier = level >> 2`, a shift, never a branch). +- A bone's address is **derived from its rest-pose centroid** by descending the + Morton quadtree of each parent's children (`morton::assign_morton_suffixes`). + +### 2.1 The one-address collapse (D-BOTHCASC, realized) + +Because each nibble is the 4×4 Morton tile of a child's position within its +parent's bounding box, **nibble-prefix containment is simultaneously**: + +1. **Partonomy containment** — `parent.address.is_ancestor_of(child.address)` + holds by construction (`address_prefix_is_partonomy_containment` test). +2. **Spatial containment** — spatially-near siblings share a longer Morton + prefix (`morton_address_encodes_laterality` test: left/right twins diverge on + the X-tile but share the region prefix). + +FMA partonomy, spatial mipmap, and the perturbation pyramid's +`(exponent, location)` are **one address**. + +### 2.2 The perturbation-shader split (OGAR vs ndarray) + +The stacked perturbation decomposes as `(exponent, location, phase, magnitude)`: + +| Component | Where | Carried by | +|---|---|---| +| `exponent` | OGAR address | the pyramid level (`tier = level >> 2`) | +| `location` | OGAR address | the 4×4 Morton tile at that level | +| `phase` | ndarray (splat) | deterministic recurrence from the address (never stored) | +| `magnitude` | ndarray (splat) | palette-quantized residual envelope (the only stored bits) | + +OGAR owns `(exponent, location)` — the *address*. ndarray owns +`(phase, magnitude)` — the *shader residual*. This keeps the OGAR job +(addressing) clean and defers the splat residuals to the SIMD layer +(architecture rule: ndarray = hardware, OGAR = address). + +### 2.3 Immutability (RESERVE-DON'T-RECLAIM) + +The Morton **routing prefix** is the address; the leaf discriminator is the +canon's 24-bit `identity` tail (`Bone::node_key`, bytes 13..16 = atlas id LE). +An interior node's address is intentionally a byte-prefix of its descendants'. +Refinement may **extend** depth (add finer Morton levels) but never rewrite an +assigned coarse nibble. The `address_stability_snapshot` + +`full_node_keys_are_globally_unique` tests pin this. + +--- + +## 3. Multi-modal projection through the bone frame + +Each modality is a forward operator onto the shared splat volume; **all register +through the bone frame** — which is *why* bones must be stable: + +| Modality | Forward operator | Registers via | +|---|---|---| +| **X-ray** | Radon line-integral (2D projection) | bones directly — X-ray *shows* bone; the natural skeletal fiducial | +| **Ultrasound** | PSF-convolved reflectivity along the beam (anisotropic Σ) | bone surfaces (strong specular returns) | +| **Doppler** | velocity field → **view-dependent appearance** | spherical-harmonics by physics — Doppler *is* view-dependent because flow is; the splat SH term | +| **ViT** | learned 2D features lifted into 3D (feature-splat) | bone landmarks as anchor tokens | + +The coronal `(x, y)` plane the Morton tiles encode (`RigidTransform::coronal`) +is exactly the plane an X-ray or anterior ultrasound sweep projects onto — so +laterality is recoverable from the address prefix alone, before any value +decode. + +--- + +## 4. Why Anatomy is its own codebook domain (`0x0A`), not Health + +The bones/skeleton concepts are **public anatomical reference structure** (the +femur exists; it is `part_of` the lower limb). A clinical *finding about* +anatomy (a fracture diagnosis on a named patient) is Health PHI; the structure +itself is not. Putting anatomy in `0x09` Health would wrongly drag it into +medcare-rs's fail-closed Health RBAC coverage set (and break its `Health == 7` +invariant). So anatomy gets `ConceptDomain::Anatomy` (`0x0A`) — reference ≠ PHI. +This is the same firewall split the splat-native arc draws: the **atlas** is +public reference (Anatomy); the **patient splat** is PHI (Health / MedCare wire). + +--- + +## 5. Honesty fence + +- **CODED [G]:** the address algebra (Morton tile encode/decode, prefix + containment, uniform 16-byte key), the partonomy tree, the clamped-anchor + invariant, unit-quaternion frames, address stability, laterality encoding — + all under `cargo test -p ogar-fma-skeleton` (16 tests). +- **v0 / proportional [H]:** rest-pose coordinates are a canonical proportional + T-pose in a body frame (origin sacral promontory; `+X` left, `+Y` superior, + `+Z` anterior; orientation identity). They carry laterality and cranio-caudal + order faithfully — enough for the Morton structure — but are **not + clinically precise** and are to be refined against a real FMA-aligned + reference mesh in the splat-native arc (D-SPLAT-8 hydrator). +- **CONJECTURE [H]:** the splat-fit convergence-stability claim (§1) is + asserted from the well-posedness argument, not yet measured. It is gated on + the splat-native arc's registration loop (`SPLAT-NATIVE-CUSTOMER.md` §3 + acceptance gate). +- **Atlas coverage:** the curated atlas is the axial skeleton (skull, full + vertebral column C1–coccyx, sternum, 12 rib pairs) + the major appendicular + bones (clavicle, scapula, humerus, radius, ulna, os coxae, femur, patella, + tibia, fibula, per side) — ~80 nodes, structured to extend to the full ~206 + without schema change. FMA ids are cross-referenced only where confidently + known; the address identity is the stable atlas id, never a fabricated FMA id. + +--- + +## 5b. The GUID tier model — `[container:member]`, located vs cascade (2026-06-23) + +The address is a uniform stack of `[256:256]` tiers, each the **same relation +at every scale**: `[container : member]` (`Galaxy:planet`, `country:city`, +`school:student`, `bodypart:bone`, `cm²:mm²`, `residue:atom`). The high byte is +the coarse container (a 256-codebook), the low byte the member attached within +it. See `src/guid.rs` (`Guid`, `Tier`, `LeafTile`). + +```text +[classid] [HEEL] [HIP] [TWIG] [LEAF=familyNode:identity] tiers, each [256:256] + 0x0A:03 … … … bodypart : bone +``` + +**The HEEL has two modes** (`HhtlMode`) — the operator's "heel" distinction: + +| mode | HEEL holds | property | who | +|---|---|---|---| +| `Located` (Cesium) | the *literal* heel — a real position (`heel:muscle`) | **preserves location**; HHTL = `spatial_tier` Morton | **bones** — they ARE the anchor | +| `Cascade` (ontology) | a classification rung (`Anatomy…PapMuscle`) | **no spatial address** — pure containment; HHTL = `ontology_tier` | **muscle / soft tissue** — classified, then projected onto | + +Both share the LEAF `familyNode:identity` and the 16-byte key; the mode is a +*reading* of the HHTL block, not a different structure. The self-speaking +ontology GUID (`ANAT0001-CARD-HERT-LVNT-PAPMUS-7A3F9C1D`) is the Cascade +reading; the bone's spatial key is the Located reading. + +**Relations live in the family-node addressing — not a `12+4` edge block.** +The fixed `12 in-family + 4 out-of-family` carve is the *pre-family-node* +taxonomy and is **superseded**. With family nodes, relations ARE the address: +a **local** relation is a shared family prefix (`Guid::same_family`); a +**cross** relation is a reference to another node's `Guid` (unbounded), not a +fixed-4 inherited-interface slot. The node stays a pure `key + value` block +with no special edge taxonomy. + +**The splat projects onto the class.** The 4D ultrasound×Doppler splat is +*mapped onto* the anatomy class addressed by this GUID — semantic + spatial + +dynamic unified on one key, no information destroyed, longitudinal tracking via +the stable identity. + +## 6. Cross-references + +- `crates/ogar-fma-skeleton/src/morton.rs` — the 4×4 Morton-tile pyramid. +- `crates/ogar-fma-skeleton/src/lib.rs` — the curated atlas + partonomy API. +- `crates/ogar-vocab/src/lib.rs` — `ConceptDomain::Anatomy`, `class_ids::BONE`. +- `docs/SPLAT-NATIVE-CUSTOMER.md` — the §6 FMA litmus + SaMD evidence base. +- `docs/DISCOVERY-MAP.md` — `D-MORTON` (§2.1), `D-FMA-SKELETON`. +- CLAUDE.md §"Tier interpretation", §"Perturbation encoding", §"Bipolar-phase + pyramid", §"3×4 PATH — UNIFORM".