use crate::error::Error;
use crate::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct Shape(
Vec<Option<usize>>,
);
impl Shape {
pub fn new(axes: Vec<Option<usize>>) -> Self {
Self(axes)
}
pub fn known(dims: &[usize]) -> Self {
Self(dims.iter().map(|&extent| Some(extent)).collect())
}
pub fn with_free_batch(dims: &[usize]) -> Self {
let mut axes: Vec<Option<usize>> = dims.iter().map(|&extent| Some(extent)).collect();
if let Some(batch) = axes.first_mut() {
*batch = None;
}
Self(axes)
}
pub fn from_batch(batch: Option<usize>, tail: &[usize]) -> Self {
let mut axes = Vec::with_capacity(tail.len() + 1);
axes.push(batch);
axes.extend(tail.iter().map(|&extent| Some(extent)));
Self(axes)
}
pub fn free_batch(&self) -> Self {
let mut axes = self.0.clone();
if let Some(batch) = axes.first_mut() {
*batch = None;
}
Self(axes)
}
pub fn rank(&self) -> usize {
self.0.len()
}
pub fn axes(&self) -> &[Option<usize>] {
&self.0
}
pub fn dims(&self) -> Option<Vec<usize>> {
self.0.iter().copied().collect()
}
pub fn check_rank(&self, layer: &str, rank: usize) -> Result<(), Error> {
if self.0.len() != rank {
return Err(Error::invalid_input(format!(
"{layer} expects an input of rank {rank}, got the shape {self} of rank {}",
self.0.len()
)));
}
Ok(())
}
pub fn check_min_rank(&self, layer: &str, min_rank: usize) -> Result<(), Error> {
if self.0.len() < min_rank {
return Err(Error::invalid_input(format!(
"{layer} expects an input of rank {min_rank} or more, got the shape {self} of \
rank {}",
self.0.len()
)));
}
Ok(())
}
pub fn split_batch(&self, layer: &str) -> Result<(Option<usize>, Vec<usize>), Error> {
let Some((batch, tail)) = self.0.split_first() else {
return Err(Error::invalid_input(format!(
"{layer} expects an input with a batch axis, got a shape of rank 0"
)));
};
let mut extents = Vec::with_capacity(tail.len());
for (position, axis) in tail.iter().enumerate() {
match axis {
Some(extent) => extents.push(*extent),
None => {
return Err(Error::invalid_input(format!(
"{layer} needs a fixed extent on axis {}, and the shape {self} leaves \
that axis free",
position + 1
)));
}
}
}
Ok((*batch, extents))
}
}
impl fmt::Display for Shape {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("(")?;
for (position, axis) in self.0.iter().enumerate() {
if position > 0 {
formatter.write_str(", ")?;
}
match axis {
Some(extent) => write!(formatter, "{extent}")?,
None => formatter.write_str("None")?,
}
}
if self.0.len() == 1 && self.0[0].is_none() {
formatter.write_str(",")?;
}
formatter.write_str(")")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_prints_a_fixed_shape() {
assert_eq!(Shape::known(&[2, 3, 4]).to_string(), "(2, 3, 4)");
assert_eq!(Shape::known(&[1, 1]).to_string(), "(1, 1)");
assert_eq!(
Shape::known(&[2, 3, 3, 3, 2]).to_string(),
"(2, 3, 3, 3, 2)"
);
}
#[test]
fn display_prints_a_free_batch_axis() {
assert_eq!(
Shape::with_free_batch(&[2, 3, 4]).to_string(),
"(None, 3, 4)"
);
assert_eq!(Shape::with_free_batch(&[7, 12]).to_string(), "(None, 12)");
}
#[test]
fn display_prints_two_free_axes() {
let shape = Shape::new(vec![None, None, Some(3)]);
assert_eq!(shape.to_string(), "(None, None, 3)");
}
#[test]
fn display_keeps_both_rank_1_forms() {
assert_eq!(Shape::known(&[4]).to_string(), "(4)");
assert_eq!(Shape::new(vec![None]).to_string(), "(None,)");
}
#[test]
fn display_prints_a_rank_0_shape() {
assert_eq!(Shape::new(Vec::new()).to_string(), "()");
}
#[test]
fn dims_needs_every_axis_fixed() {
assert_eq!(Shape::known(&[2, 3]).dims(), Some(vec![2, 3]));
assert_eq!(Shape::with_free_batch(&[2, 3]).dims(), None);
}
#[test]
fn with_free_batch_frees_only_axis_0() {
let shape = Shape::with_free_batch(&[9, 3, 4]);
assert_eq!(shape.axes(), &[None, Some(3), Some(4)]);
assert_eq!(shape.rank(), 3);
}
#[test]
fn free_batch_frees_only_axis_0() {
let shape = Shape::known(&[9, 3, 4]);
assert_eq!(shape.free_batch().axes(), &[None, Some(3), Some(4)]);
assert_eq!(Shape::new(Vec::new()).free_batch(), Shape::new(Vec::new()));
}
#[test]
fn with_free_batch_accepts_an_empty_list() {
assert_eq!(Shape::with_free_batch(&[]), Shape::new(Vec::new()));
}
#[test]
fn from_batch_rejoins_the_batch_axis() {
assert_eq!(
Shape::from_batch(None, &[3, 4]),
Shape::new(vec![None, Some(3), Some(4)])
);
assert_eq!(Shape::from_batch(Some(2), &[3]), Shape::known(&[2, 3]));
}
#[test]
fn check_rank_reports_the_rank_it_wanted() {
let shape = Shape::with_free_batch(&[2, 3]);
assert!(shape.check_rank("Conv2D", 2).is_ok());
let message = shape.check_rank("Conv2D", 4).unwrap_err().to_string();
assert!(message.contains("Conv2D"), "{message}");
assert!(message.contains("rank 4"), "{message}");
assert!(message.contains("(None, 3)"), "{message}");
}
#[test]
fn check_min_rank_accepts_a_higher_rank() {
let shape = Shape::with_free_batch(&[2, 3, 4]);
assert!(shape.check_min_rank("Dense", 2).is_ok());
assert!(shape.check_min_rank("Dense", 3).is_ok());
let message = shape.check_min_rank("Dense", 4).unwrap_err().to_string();
assert!(message.contains("Dense"), "{message}");
assert!(message.contains("rank 4 or more"), "{message}");
}
#[test]
fn split_batch_keeps_the_batch_axis() {
let (batch, tail) = Shape::with_free_batch(&[2, 3, 4])
.split_batch("Dense")
.unwrap();
assert_eq!(batch, None);
assert_eq!(tail, vec![3, 4]);
let (batch, tail) = Shape::known(&[2, 3]).split_batch("Dense").unwrap();
assert_eq!(batch, Some(2));
assert_eq!(tail, vec![3]);
}
#[test]
fn split_batch_refuses_a_free_axis() {
let shape = Shape::new(vec![None, None, Some(3)]);
let message = shape.split_batch("Conv1D").unwrap_err().to_string();
assert!(message.contains("Conv1D"), "{message}");
assert!(message.contains("axis 1"), "{message}");
}
#[test]
fn split_batch_refuses_a_rank_0_shape() {
let message = Shape::new(Vec::new())
.split_batch("Flatten")
.unwrap_err()
.to_string();
assert!(message.contains("batch axis"), "{message}");
}
}