serde-onnx 0.1.0

Strongly typed Rust data structures for ONNX models, with export and import
Documentation
use proptest::prelude::*;

use serde_onnx::ir::{ElemType, Scalar, Tensor, TensorData};

fn le_roundtrip<T: Scalar + PartialEq + std::fmt::Debug>(values: Vec<T>) {
    let mut bytes = Vec::with_capacity(values.len() * T::SIZE);
    for v in &values {
        bytes.extend_from_slice(&v.to_le_bytes()[..T::SIZE]);
    }
    let mut decoded = Vec::with_capacity(values.len());
    for chunk in bytes.chunks(T::SIZE) {
        decoded.push(T::from_le_bytes(chunk).expect("decode must succeed"));
    }
    assert_eq!(decoded, values);
}

proptest! {
    #[test]
    fn le_roundtrip_f32(values in prop::collection::vec(any::<f32>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_f64(values in prop::collection::vec(any::<f64>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_i8(values in prop::collection::vec(any::<i8>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_i16(values in prop::collection::vec(any::<i16>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_i32(values in prop::collection::vec(any::<i32>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_i64(values in prop::collection::vec(any::<i64>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_u8(values in prop::collection::vec(any::<u8>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_u16(values in prop::collection::vec(any::<u16>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_u32(values in prop::collection::vec(any::<u32>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_u64(values in prop::collection::vec(any::<u64>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn le_roundtrip_bool(values in prop::collection::vec(any::<bool>(), 0..64)) {
        le_roundtrip(values);
    }

    #[test]
    fn as_slice_exact_is_lossless(values in prop::collection::vec(any::<f64>(), 0..64)) {
        let tensor = Tensor::new("t", ElemType::Double, vec![], TensorData::F64(values.clone()));
        let got = tensor.as_slice::<f64>().expect("exact access must succeed");
        assert_eq!(got, &values[..]);
    }

    #[test]
    fn to_vec_i32_i64_lossless(values in prop::collection::vec(any::<i32>(), 0..64)) {
        let tensor = Tensor::new("t", ElemType::Int32, vec![], TensorData::I32(values.clone()));
        let got = tensor.to_vec::<i64>().expect("i32 to i64 is lossless");
        assert_eq!(got, values.iter().map(|&v| v as i64).collect::<Vec<i64>>());
    }

    #[test]
    fn to_vec_u8_i64_lossless(values in prop::collection::vec(any::<u8>(), 0..64)) {
        let tensor = Tensor::new("t", ElemType::Uint8, vec![], TensorData::U8(values.clone()));
        let got = tensor.to_vec::<i64>().expect("u8 to i64 is lossless");
        assert_eq!(got, values.iter().map(|&v| v as i64).collect::<Vec<i64>>());
    }
}

mod unit {
    use super::*;

    #[test]
    fn as_slice_dtype_mismatch_fails() {
        let tensor = Tensor::new("t", ElemType::Float, vec![], TensorData::F32(vec![1.0]));
        let err = tensor.as_slice::<f64>().unwrap_err();
        assert_eq!(
            err,
            serde_onnx::ir::TensorError::TypeError {
                expected: ElemType::Double,
                got: ElemType::Float,
            }
        );
    }

    #[test]
    fn to_vec_float_to_int_rejects_fraction() {
        let tensor = Tensor::new("t", ElemType::Float, vec![], TensorData::F32(vec![1.5]));
        let err = tensor.to_vec::<i64>().expect_err("must reject fraction");
        match err {
            serde_onnx::ir::TensorError::CastError {
                index,
                from,
                to,
                reason,
            } => {
                assert_eq!(index, 0);
                assert_eq!(from, ElemType::Float);
                assert_eq!(to, ElemType::Int64);
                assert_eq!(reason, serde_onnx::ir::CastLossReason::NonIntegral);
            }
            other => panic!("expected CastError, got {other:?}"),
        }
    }

    #[test]
    fn to_vec_float_to_int_rejects_out_of_range() {
        let tensor = Tensor::new(
            "t",
            ElemType::Float,
            vec![],
            TensorData::F32(vec![f32::INFINITY]),
        );
        let err = tensor.to_vec::<i32>().expect_err("must reject inf");
        match err {
            serde_onnx::ir::TensorError::CastError { index, reason, .. } => {
                assert_eq!(index, 0);
                assert_eq!(reason, serde_onnx::ir::CastLossReason::OutOfRange);
            }
            other => panic!("expected CastError, got {other:?}"),
        }
    }

    #[test]
    fn to_vec_f64_to_u64_large_fails() {
        let tensor = Tensor::new(
            "t",
            ElemType::Double,
            vec![],
            TensorData::F64(vec![2f64.powi(64)]),
        );
        assert!(tensor.to_vec::<u64>().is_err());
    }

    #[test]
    fn to_vec_f64_to_f32_rejects_overflow() {
        let tensor = Tensor::new("t", ElemType::Double, vec![], TensorData::F64(vec![1e300]));
        let err = tensor
            .to_vec::<f32>()
            .expect_err("must reject f64 -> f32 overflow");
        match err {
            serde_onnx::ir::TensorError::CastError { reason, .. } => {
                assert_eq!(reason, serde_onnx::ir::CastLossReason::OutOfRange);
            }
            other => panic!("expected CastError, got {other:?}"),
        }
    }

    #[test]
    fn to_vec_string_always_fails() {
        let tensor = Tensor::new(
            "t",
            ElemType::String,
            vec![],
            TensorData::String(vec!["x".to_string()]),
        );
        assert!(matches!(
            tensor.to_vec::<f32>(),
            Err(serde_onnx::ir::TensorError::TypeError { .. })
        ));
    }

    #[test]
    fn f16_mismatch_fails() {
        let tensor = Tensor::new("t", ElemType::Float16, vec![], TensorData::F16(vec![0]));
        assert!(tensor.bf16_to_f32_vec().is_err());
        assert!(tensor.f16_to_f32_vec().is_ok());
    }

    #[test]
    fn to_vec_bool_exact() {
        let tensor = Tensor::new(
            "t",
            ElemType::Bool,
            vec![],
            TensorData::Bool(vec![true, false]),
        );
        assert_eq!(tensor.as_slice::<bool>().unwrap(), &[true, false]);
    }

    #[test]
    fn f16_bits_roundtrip_f32() {
        let vals = vec![0.0f32, 1.0, -2.5, 65504.0];
        let tensor = Tensor::new(
            "t",
            ElemType::Float16,
            vec![],
            TensorData::f16_from_f32(&vals),
        );
        assert_eq!(tensor.f16_bits().unwrap().len(), 4);
        assert_eq!(
            tensor.f16_to_f32_vec().unwrap(),
            vec![0.0, 1.0, -2.5, 65504.0]
        );
    }

    #[test]
    fn bf16_bits_roundtrip_f32() {
        let vals = vec![0.0f32, 1.0, -2.5, 3.0];
        let tensor = Tensor::new(
            "t",
            ElemType::Bfloat16,
            vec![],
            TensorData::bf16_from_f32(&vals),
        );
        assert_eq!(tensor.bf16_bits().unwrap().len(), 4);
        assert_eq!(tensor.bf16_to_f32_vec().unwrap(), vec![0.0, 1.0, -2.5, 3.0]);
    }
}