soma-som-core 0.1.0

Universal soma(som) structural primitives — Quad / Tree / Ring / Genesis / Fingerprint / TemporalLedger / CrossingRecord
Documentation
// SPDX-License-Identifier: LGPL-3.0-only
#![allow(missing_docs)]

//! Position addressing: (unit, layer, element) triples.
//!
//! ## Spec traceability
//! - Spec §8.1: Each SOM key position is addressed as (u, l, e)
//! - Spec §8.2: 72 SOM key positions (Table 5)
//! - Spec §8.3: 18 SOMA key positions (Table 6)

use serde::{Deserialize, Serialize};

use crate::types::{Element, Layer, UnitId};

/// A position in the SOMA/SOM structural skeleton.
///
/// ## SOM positions (72)
///
/// Addressed as `(unit, Some(layer), element)` where unit ∈ {FU..HU},
/// layer ∈ {Data, Server, Client, Interface}, element ∈ {Root, Pointer, Tree}.
/// These are the awareness-relevant positions (Spec §8.2).
///
/// ## SOMA positions (18)
///
/// Addressed as `(unit, None, element)` where the layer is `None` because
/// SOMA Quads exist on the vertical axis, not within the SOM layer chain.
/// These carry unit-level structural state (Spec §8.3).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Position {
    pub unit: UnitId,
    /// `Some(layer)` for SOM positions, `None` for SOMA positions.
    pub layer: Option<Layer>,
    pub element: Element,
}

impl Position {
    /// Create a SOM position.
    pub fn som(unit: UnitId, layer: Layer, element: Element) -> Self {
        Self {
            unit,
            layer: Some(layer),
            element,
        }
    }

    /// Create a SOMA position.
    pub fn soma(unit: UnitId, element: Element) -> Self {
        Self {
            unit,
            layer: None,
            element,
        }
    }

    /// Is this a SOM position (awareness-relevant)?
    pub fn is_som(&self) -> bool {
        self.layer.is_some()
    }

    /// Is this a SOMA position (unit-level structural)?
    pub fn is_soma(&self) -> bool {
        self.layer.is_none()
    }

    /// Linear index into the 72 SOM positions (k₁..k₇₂ in Spec Table 5).
    ///
    /// Returns `None` for SOMA positions.
    ///
    /// Ordering: FU.Data.R=0, FU.Data.P=1, FU.Data.T=2,
    ///           FU.Server.R=3, ... HU.Interface.T=71
    pub fn som_index(&self) -> Option<usize> {
        let layer = self.layer?;
        Some(
            self.unit.index() * (Layer::COUNT * Element::COUNT)
                + layer.index() * Element::COUNT
                + self.element.index(),
        )
    }

    /// Linear index into the 18 SOMA positions (k₇₃..k₉₀ in Spec Table 6).
    ///
    /// Returns `None` for SOM positions.
    pub fn soma_index(&self) -> Option<usize> {
        if self.layer.is_some() {
            return None;
        }
        Some(self.unit.index() * Element::COUNT + self.element.index())
    }

    /// Linear index into the full 90-position skeleton (k₁..k₉₀).
    ///
    /// SOM positions occupy indices 0..71, SOMA positions occupy 72..89.
    pub fn global_index(&self) -> usize {
        if let Some(layer) = self.layer {
            self.unit.index() * (Layer::COUNT * Element::COUNT)
                + layer.index() * Element::COUNT
                + self.element.index()
        } else {
            72 + self.unit.index() * Element::COUNT + self.element.index()
        }
    }

    /// Generate all 72 SOM positions in canonical order.
    pub fn all_som() -> Vec<Position> {
        let mut positions = Vec::with_capacity(72);
        for &unit in &UnitId::ALL {
            for &layer in &Layer::ALL {
                for &element in &Element::ALL {
                    positions.push(Position::som(unit, layer, element));
                }
            }
        }
        positions
    }

    /// Generate all 18 SOMA positions in canonical order.
    pub fn all_soma() -> Vec<Position> {
        let mut positions = Vec::with_capacity(18);
        for &unit in &UnitId::ALL {
            for &element in &Element::ALL {
                positions.push(Position::soma(unit, element));
            }
        }
        positions
    }

    /// Generate all 90 positions in canonical order (SOM then SOMA).
    pub fn all() -> Vec<Position> {
        let mut positions = Position::all_som();
        positions.extend(Position::all_soma());
        positions
    }
}

impl std::fmt::Display for Position {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.layer {
            Some(layer) => write!(f, "{}.{}.{:?}", self.unit, layer, self.element),
            None => write!(f, "{}.SOMA.{:?}", self.unit, self.element),
        }
    }
}

// inline: exercises module-private items via super::*
#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{SOM_POSITION_COUNT, SOMA_POSITION_COUNT, TOTAL_POSITION_COUNT};

    #[test]
    fn all_som_has_72_positions() {
        assert_eq!(Position::all_som().len(), SOM_POSITION_COUNT);
    }

    #[test]
    fn all_soma_has_18_positions() {
        assert_eq!(Position::all_soma().len(), SOMA_POSITION_COUNT);
    }

    #[test]
    fn all_has_90_positions() {
        assert_eq!(Position::all().len(), TOTAL_POSITION_COUNT);
    }

    #[test]
    fn som_indices_are_contiguous_0_to_71() {
        let positions = Position::all_som();
        for (i, pos) in positions.iter().enumerate() {
            assert_eq!(pos.som_index(), Some(i));
            assert_eq!(pos.global_index(), i);
        }
    }

    #[test]
    fn soma_indices_are_contiguous_0_to_17() {
        let positions = Position::all_soma();
        for (i, pos) in positions.iter().enumerate() {
            assert_eq!(pos.soma_index(), Some(i));
            assert_eq!(pos.global_index(), 72 + i);
        }
    }

    #[test]
    fn global_indices_are_unique() {
        let all = Position::all();
        let mut indices: Vec<usize> = all.iter().map(|p| p.global_index()).collect();
        indices.sort();
        indices.dedup();
        assert_eq!(indices.len(), TOTAL_POSITION_COUNT);
    }

    #[test]
    fn first_position_is_fu_data_root() {
        let first = Position::all_som()[0];
        assert_eq!(first, Position::som(UnitId::FU, Layer::Data, Element::Root));
        assert_eq!(first.global_index(), 0);
    }

    #[test]
    fn last_som_position_is_hu_interface_tree() {
        let last = Position::all_som().last().copied().unwrap();
        assert_eq!(
            last,
            Position::som(UnitId::HU, Layer::Interface, Element::Tree)
        );
        assert_eq!(last.global_index(), 71);
    }
}