phoxal 0.7.0

Phoxal — production-oriented autonomous robot framework (engine, model, typed bus, contracts).
Documentation
pub const SCHEMA_NAME: &str = "phoxal-api-perception/v1";
pub const SCHEMA_VERSION: u32 = 1;

use std::fmt;

use crate::api::v1::frame::FrameId;
use crate::api::v1::localize::LocalizationRevisionId;
use crate::api::v1::map::MapRevisionId;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct BoundingBox {
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Detection {
    pub class_label: String,
    pub class_id: u32,
    pub confidence: f32,
    pub bbox: BoundingBox,
    pub anchor_3d_m: Option<[f64; 3]>,
    pub source_frame_id: FrameId,
    pub tracker_id: Option<u64>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevisionLinkage {
    pub localize_revision: LocalizationRevisionId,
    pub map_revision: MapRevisionId,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Detections {
    pub detections: Vec<Detection>,
    pub localize_revision: LocalizationRevisionId,
    pub map_revision: MapRevisionId,
    pub detector_id: String,
}

impl Detections {
    #[must_use]
    pub const fn revision_linkage(&self) -> RevisionLinkage {
        RevisionLinkage {
            localize_revision: self.localize_revision,
            map_revision: self.map_revision,
        }
    }

    pub fn validate_revision_linkage(
        &self,
        expected: RevisionLinkage,
    ) -> Result<RevisionLinkage, RevisionMismatch> {
        let actual = self.revision_linkage();
        if actual == expected {
            Ok(actual)
        } else {
            Err(RevisionMismatch { expected, actual })
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevisionMismatch {
    pub expected: RevisionLinkage,
    pub actual: RevisionLinkage,
}

impl fmt::Display for RevisionMismatch {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "perception detection revisions mismatch: expected localize {:?} map {:?}, got localize {:?} map {:?}",
            self.expected.localize_revision,
            self.expected.map_revision,
            self.actual.localize_revision,
            self.actual.map_revision
        )
    }
}

impl std::error::Error for RevisionMismatch {}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PerceptionState {
    pub health: DetectorHealth,
    pub backend: String,
    pub model_id: String,
    pub weights_version: String,
    pub inference_budget_headroom: f32,
    pub cadence_hz: f32,
    pub dropped_frames: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DetectorHealth {
    Healthy,
    Degraded(PerceptionDegradedReason),
    Stopped(PerceptionStoppedReason),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PerceptionDegradedReason {
    InferenceBudgetExceeded,
    SourceStale,
    LocalizationDegraded,
    BackendThrottled,
    ConfidenceCollapse,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PerceptionStoppedReason {
    ModelLoadFailed,
    ComputeUnavailable,
    SourceUnavailable,
    SupervisorStopped,
    BackendError,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TrackedObservation {
    pub tracker_id: u64,
    pub last_seen_ns: u64,
    pub localize_revision: LocalizationRevisionId,
    pub map_revision: MapRevisionId,
    pub anchor_3d_m: Option<[f64; 3]>,
    pub source_frame_id: FrameId,
}

#[cfg(test)]
mod tests {
    use crate::api::v1::frame::FrameId;
    use crate::api::v1::localize::LocalizationRevisionId;
    use crate::api::v1::map::MapRevisionId;

    use super::{BoundingBox, Detection, Detections, RevisionLinkage};

    #[test]
    fn rejects_mismatched_revision_linkage() {
        let detections = detections(localize(7, 9), map(3, 5));
        let mismatch = detections
            .validate_revision_linkage(RevisionLinkage {
                localize_revision: localize(7, 10),
                map_revision: map(3, 5),
            })
            .expect_err("mismatched localize revision should be rejected");

        assert_eq!(mismatch.actual.localize_revision, localize(7, 9));
        assert_eq!(mismatch.actual.map_revision, map(3, 5));
    }

    #[test]
    fn round_trips_revision_linkage_through_detection_batch() {
        let detections = detections(localize(2, 4), map(9, 10));
        let encoded = serde_json::to_string(&detections).expect("serialize detections");
        let decoded: Detections = serde_json::from_str(&encoded).expect("deserialize detections");

        assert_eq!(
            decoded
                .validate_revision_linkage(RevisionLinkage {
                    localize_revision: localize(2, 4),
                    map_revision: map(9, 10),
                })
                .expect("matching revisions"),
            RevisionLinkage {
                localize_revision: localize(2, 4),
                map_revision: map(9, 10),
            }
        );
    }

    fn detections(
        localize_revision: LocalizationRevisionId,
        map_revision: MapRevisionId,
    ) -> Detections {
        Detections {
            detections: vec![Detection {
                class_label: "crate".to_string(),
                class_id: 12,
                confidence: 0.75,
                bbox: BoundingBox {
                    x: 10.0,
                    y: 20.0,
                    width: 30.0,
                    height: 40.0,
                },
                anchor_3d_m: Some([1.0, 2.0, 3.0]),
                source_frame_id: FrameId::new("front_camera__camera_link"),
                tracker_id: None,
            }],
            localize_revision,
            map_revision,
            detector_id: "placeholder".to_string(),
        }
    }

    const fn localize(epoch: u64, sequence: u64) -> LocalizationRevisionId {
        LocalizationRevisionId { epoch, sequence }
    }

    const fn map(epoch: u64, sequence: u64) -> MapRevisionId {
        MapRevisionId { epoch, sequence }
    }
}

#[cfg(test)]
mod v1_version_tests {
    use super::{SCHEMA_NAME, SCHEMA_VERSION};

    #[test]
    fn api_contract_version_is_stable() {
        assert_eq!(SCHEMA_NAME, "phoxal-api-perception/v1");
        assert_eq!(SCHEMA_VERSION, 1);
    }
}