helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
//! The continuous latent payload: a finite, rank-2 `[frames, dim]` sequence.

use serde::{Deserialize, Serialize};

use crate::error::{Error, Result};
use crate::tensor::{Tensor, checked_shape_len};

use super::kind::Framed;

/// A `[frames, dim]` continuous latent sequence.
///
/// Invariants, enforced by every constructor and re-checked on
/// deserialization: rank exactly 2, `dim ≥ 1`, and finite values. Rank-2-ness
/// and finiteness are *this type's* facts — the ≥5 call sites that used to
/// re-derive "is this really `[frames, dim]`?" and the scattered NaN scans
/// are deleted, not relocated. Zero frames is permitted (emptiness is a
/// downstream concern; structure is the invariant here).
///
/// Serialization is human-readable (`{"shape": [frames, dim], "values":
/// […]}`), which is what made the old `LatentExport` scaffold exist; the
/// type now carries its own export form.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(into = "FrameSeqRepr", try_from = "FrameSeqRepr")]
pub struct FrameSeq {
    frames: usize,
    dim: usize,
    values: Vec<f32>,
}

impl FrameSeq {
    /// Build a sequence from its dimensions and row-major values.
    ///
    /// Returns [`Error::Validation`] if `dim` is zero or `frames × dim`
    /// overflows, [`Error::Shape`] if `values.len() ≠ frames × dim`, and
    /// [`Error::NonFinite`] on NaN/infinite values.
    pub fn new(frames: usize, dim: usize, values: Vec<f32>) -> Result<Self> {
        if dim == 0 {
            return Err(Error::validation("a frame sequence needs dim ≥ 1"));
        }
        let expected = checked_shape_len(&[frames, dim]).ok_or_else(|| {
            Error::validation(format!("frame-seq shape [{frames}, {dim}] overflows usize"))
        })?;
        if values.len() != expected {
            return Err(Error::shape(vec![frames, dim], vec![values.len()]));
        }
        if !values.iter().all(|v| v.is_finite()) {
            return Err(Error::NonFinite {
                context: "frame sequence values",
            });
        }
        Ok(Self {
            frames,
            dim,
            values,
        })
    }

    /// Adopt a raw [`Tensor`], proving the frame-sequence invariants.
    ///
    /// Returns [`Error::Shape`] for a non-rank-2 tensor and the same
    /// rejections as [`new`](Self::new) otherwise.
    pub fn from_tensor(tensor: Tensor) -> Result<Self> {
        match *tensor.shape() {
            [frames, dim] => {
                let values = tensor.data().to_vec();
                Self::new(frames, dim, values)
            }
            ref shape => Err(Error::shape(vec![0, 0], shape.to_vec())),
        }
    }

    /// Wrap host-side `TensorData` returning from a device (the boundary
    /// where finiteness is proven once, instead of at every consumer).
    pub fn from_data(data: burn_tensor::TensorData) -> Result<Self> {
        Self::from_tensor(Tensor::from_data(data)?)
    }

    /// Number of frames (rows). May be zero.
    pub fn frames(&self) -> usize {
        self.frames
    }

    /// Latent width (columns). Always ≥ 1.
    pub fn dim(&self) -> usize {
        self.dim
    }

    /// The row-major values, length `frames × dim`, all finite.
    pub fn values(&self) -> &[f32] {
        &self.values
    }

    /// One frame's values. Panics out of bounds, as slice indexing does.
    pub fn frame(&self, index: usize) -> &[f32] {
        &self.values[index * self.dim..(index + 1) * self.dim]
    }

    /// View as the raw carrier for device upload.
    pub fn to_tensor(&self) -> Tensor {
        Tensor::new([self.frames, self.dim], self.values.clone())
            .expect("frame-seq invariants imply a valid tensor")
    }

    /// Consume into host-side `TensorData` — the input to
    /// `burn::tensor::Tensor::from_data`.
    pub fn into_data(self) -> burn_tensor::TensorData {
        burn_tensor::TensorData::new(self.values, vec![self.frames, self.dim])
    }
}

impl Framed for FrameSeq {
    fn frames(&self) -> usize {
        self.frames
    }

    fn slice_frames(&self, start: usize, len: usize) -> Self {
        let values = self.values[start * self.dim..(start + len) * self.dim].to_vec();
        Self {
            frames: len,
            dim: self.dim,
            values,
        }
    }
}

/// Wire form: human-readable shape + values, routed back through validation.
#[derive(Serialize, Deserialize)]
struct FrameSeqRepr {
    shape: [usize; 2],
    values: Vec<f32>,
}

impl From<FrameSeq> for FrameSeqRepr {
    fn from(seq: FrameSeq) -> Self {
        Self {
            shape: [seq.frames, seq.dim],
            values: seq.values,
        }
    }
}

impl TryFrom<FrameSeqRepr> for FrameSeq {
    type Error = Error;

    fn try_from(repr: FrameSeqRepr) -> Result<Self> {
        Self::new(repr.shape[0], repr.shape[1], repr.values)
    }
}

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

    #[test]
    fn invariants_hold_at_every_gate() {
        assert!(FrameSeq::new(2, 0, vec![]).is_err());
        assert!(matches!(
            FrameSeq::new(2, 2, vec![0.0; 3]),
            Err(Error::Shape { .. })
        ));
        assert!(matches!(
            FrameSeq::new(1, 2, vec![0.0, f32::NAN]),
            Err(Error::NonFinite { .. })
        ));
        assert!(FrameSeq::new(0, 3, vec![]).is_ok()); // empty is structural, not illegal
        assert!(FrameSeq::new(usize::MAX, 2, vec![]).is_err()); // overflow guard
    }

    #[test]
    fn from_tensor_demands_rank_two() {
        let rank1 = Tensor::vector([1.0, 2.0]);
        assert!(matches!(
            FrameSeq::from_tensor(rank1),
            Err(Error::Shape { .. })
        ));
        let rank2 = Tensor::new([2, 2], vec![1.0, 2.0, 3.0, 4.0]).unwrap();
        let seq = FrameSeq::from_tensor(rank2).unwrap();
        assert_eq!((seq.frames(), seq.dim()), (2, 2));
    }

    #[test]
    fn rows_and_slices() {
        let seq = FrameSeq::new(3, 2, vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
        assert_eq!(seq.frame(1), &[2.0, 3.0]);
        let mid = seq.slice_frames(1, 2);
        assert_eq!((mid.frames(), mid.dim()), (2, 2));
        assert_eq!(mid.values(), &[2.0, 3.0, 4.0, 5.0]);
    }

    #[test]
    fn serde_is_human_readable_and_gated() {
        let seq = FrameSeq::new(1, 2, vec![0.25, -0.5]).unwrap();
        let json = serde_json::to_string(&seq).unwrap();
        assert_eq!(json, r#"{"shape":[1,2],"values":[0.25,-0.5]}"#);
        assert_eq!(serde_json::from_str::<FrameSeq>(&json).unwrap(), seq);

        // Hand-built bad records fail closed at deserialization.
        assert!(serde_json::from_str::<FrameSeq>(r#"{"shape":[2,2],"values":[0.0]}"#).is_err());
        assert!(serde_json::from_str::<FrameSeq>(r#"{"shape":[1,1],"values":[null]}"#).is_err());
    }

    #[test]
    fn device_boundary_round_trip() {
        let seq = FrameSeq::new(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
        let back = FrameSeq::from_data(seq.clone().into_data()).unwrap();
        assert_eq!(back, seq);
    }
}