Skip to main content

neco_linear_types/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![forbid(unsafe_code)]
3#![doc = include_str!("../README.md")]
4
5extern crate alloc;
6
7use alloc::vec::Vec;
8use core::fmt;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum LinearError {
12    DimensionMismatch {
13        expected_rows: usize,
14        expected_columns: usize,
15        actual_rows: usize,
16        actual_columns: usize,
17    },
18    IndexOutOfBounds {
19        axis: &'static str,
20        index: usize,
21        bound: usize,
22    },
23    StorageLengthMismatch {
24        expected: usize,
25        actual: usize,
26    },
27    CapacityOverflow {
28        requested: usize,
29    },
30    AllocationFailure {
31        requested: usize,
32    },
33    InvalidStorage {
34        reason: &'static str,
35    },
36}
37
38impl fmt::Display for LinearError {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Self::DimensionMismatch {
42                expected_rows,
43                expected_columns,
44                actual_rows,
45                actual_columns,
46            } => write!(
47                formatter,
48                "dimension mismatch: expected {expected_rows}x{expected_columns}, got {actual_rows}x{actual_columns}"
49            ),
50            Self::IndexOutOfBounds { axis, index, bound } => {
51                write!(formatter, "{axis} index {index} is out of bounds for {bound}")
52            }
53            Self::StorageLengthMismatch { expected, actual } => {
54                write!(formatter, "storage length mismatch: expected {expected}, got {actual}")
55            }
56            Self::CapacityOverflow { requested } => {
57                write!(formatter, "capacity overflow for {requested} elements")
58            }
59            Self::AllocationFailure { requested } => {
60                write!(formatter, "allocation failed for {requested} elements")
61            }
62            Self::InvalidStorage { reason } => write!(formatter, "invalid storage: {reason}"),
63        }
64    }
65}
66
67#[cfg(feature = "std")]
68impl std::error::Error for LinearError {}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct Shape {
72    rows: usize,
73    columns: usize,
74}
75
76impl Shape {
77    pub const fn new(rows: usize, columns: usize) -> Self {
78        Self { rows, columns }
79    }
80
81    pub const fn rows(self) -> usize {
82        self.rows
83    }
84
85    pub const fn columns(self) -> usize {
86        self.columns
87    }
88
89    pub fn element_count(self) -> Result<usize, LinearError> {
90        self.rows
91            .checked_mul(self.columns)
92            .ok_or(LinearError::CapacityOverflow {
93                requested: usize::MAX,
94            })
95    }
96
97    pub fn row_index(self, index: usize) -> Result<RowIndex, LinearError> {
98        if index < self.rows {
99            Ok(RowIndex(index))
100        } else {
101            Err(LinearError::IndexOutOfBounds {
102                axis: "row",
103                index,
104                bound: self.rows,
105            })
106        }
107    }
108
109    pub fn column_index(self, index: usize) -> Result<ColumnIndex, LinearError> {
110        if index < self.columns {
111            Ok(ColumnIndex(index))
112        } else {
113            Err(LinearError::IndexOutOfBounds {
114                axis: "column",
115                index,
116                bound: self.columns,
117            })
118        }
119    }
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
123pub struct RowIndex(usize);
124
125impl RowIndex {
126    pub const fn value(self) -> usize {
127        self.0
128    }
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
132pub struct ColumnIndex(usize);
133
134impl ColumnIndex {
135    pub const fn value(self) -> usize {
136        self.0
137    }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct Vector<T> {
142    values: Vec<T>,
143}
144
145impl<T> Vector<T> {
146    pub fn try_from_vec(values: Vec<T>) -> Result<Self, LinearError> {
147        if values.len() > isize::MAX as usize {
148            return Err(LinearError::InvalidStorage {
149                reason: "vector length exceeds the representable allocation limit",
150            });
151        }
152        Ok(Self { values })
153    }
154
155    pub fn try_zeros(length: usize, value: T) -> Result<Self, LinearError>
156    where
157        T: Clone,
158    {
159        let mut values = Vec::new();
160        values
161            .try_reserve_exact(length)
162            .map_err(|_| LinearError::AllocationFailure { requested: length })?;
163        values.resize(length, value);
164        Self::try_from_vec(values)
165    }
166
167    pub fn len(&self) -> usize {
168        self.values.len()
169    }
170
171    pub fn is_empty(&self) -> bool {
172        self.values.is_empty()
173    }
174
175    pub fn value(&self, index: usize) -> Result<&T, LinearError> {
176        self.values.get(index).ok_or(LinearError::IndexOutOfBounds {
177            axis: "vector",
178            index,
179            bound: self.values.len(),
180        })
181    }
182
183    pub fn values(&self) -> &[T] {
184        &self.values
185    }
186
187    pub fn into_values(self) -> Vec<T> {
188        self.values
189    }
190}
191
192pub trait LinearOperator<T> {
193    fn domain(&self) -> usize;
194
195    fn codomain(&self) -> usize;
196
197    fn apply(&self, input: &Vector<T>) -> Result<Vector<T>, LinearError>;
198}
199
200#[cfg(test)]
201mod tests {
202    use alloc::vec;
203
204    use super::{LinearError, LinearOperator, Shape, Vector};
205
206    #[test]
207    fn shape_rejects_invalid_indices_and_detects_size_overflow() {
208        let shape = Shape::new(2, 3);
209        assert_eq!(shape.element_count(), Ok(6));
210        assert!(matches!(
211            shape.row_index(2),
212            Err(LinearError::IndexOutOfBounds { axis: "row", .. })
213        ));
214        assert!(matches!(
215            shape.column_index(3),
216            Err(LinearError::IndexOutOfBounds { axis: "column", .. })
217        ));
218        assert!(matches!(
219            Shape::new(usize::MAX, 2).element_count(),
220            Err(LinearError::CapacityOverflow { .. })
221        ));
222    }
223
224    #[test]
225    fn vector_preserves_length_and_values() {
226        let vector = Vector::try_zeros(3, 7_u8).expect("small allocation succeeds");
227        assert_eq!(vector.len(), 3);
228        assert_eq!(vector.values(), &[7, 7, 7]);
229        assert_eq!(vector.into_values(), vec![7, 7, 7]);
230    }
231
232    struct TestOperator;
233
234    impl LinearOperator<i32> for TestOperator {
235        fn domain(&self) -> usize {
236            2
237        }
238
239        fn codomain(&self) -> usize {
240            1
241        }
242
243        fn apply(&self, input: &Vector<i32>) -> Result<Vector<i32>, LinearError> {
244            if input.len() != self.domain() {
245                return Err(LinearError::StorageLengthMismatch {
246                    expected: self.domain(),
247                    actual: input.len(),
248                });
249            }
250            let output = Vector::try_from_vec(vec![*input.value(0)?])?;
251            if output.len() != self.codomain() {
252                return Err(LinearError::StorageLengthMismatch {
253                    expected: self.codomain(),
254                    actual: output.len(),
255                });
256            }
257            Ok(output)
258        }
259    }
260
261    #[test]
262    fn operator_trait_checks_input_and_output_storage_lengths() {
263        let operator = TestOperator;
264        let input = Vector::try_from_vec(vec![1_i32, 2]).expect("valid storage succeeds");
265        assert_eq!(operator.domain(), 2);
266        assert_eq!(operator.codomain(), 1);
267        assert_eq!(operator.apply(&input).expect("valid dimensions").len(), 1);
268
269        let short = Vector::try_from_vec(vec![1_i32]).expect("valid storage succeeds");
270        assert!(matches!(
271            operator.apply(&short),
272            Err(LinearError::StorageLengthMismatch {
273                expected: 2,
274                actual: 1
275            })
276        ));
277    }
278}