use super::merge_layer_base_functions;
use crate::error::Error;
use crate::neural_network::layers::ParamCounts;
use crate::neural_network::layers::validation::start_build_many;
use crate::neural_network::traits::{Arity, Layer, LayerBase};
use crate::neural_network::{Ctx, Shape, Tensor};
use ndarray::{Axis, Slice};
#[derive(Debug)]
pub struct Concatenate {
axis: i32,
built: Option<Vec<Shape>>,
}
impl Concatenate {
pub fn new(axis: i32) -> Self {
Self { axis, built: None }
}
fn resolve_axis(&self, rank: usize) -> Result<usize, Error> {
if rank == 0 {
return Err(Error::invalid_input(
"Concatenate joins inputs of rank 1 or more, and it received a shape of rank 0",
));
}
let resolved = if self.axis < 0 {
self.axis as i64 + rank as i64
} else {
self.axis as i64
};
if resolved < 0 || resolved >= rank as i64 {
return Err(Error::invalid_input(format!(
"Concatenate joins on the axis {}, which falls outside an input of rank {rank}. \
The axis runs from {} through {}",
self.axis,
-(rank as i64),
rank as i64 - 1
)));
}
Ok(resolved as usize)
}
fn joined_shape(&self, inputs: &[Shape]) -> Result<Shape, Error> {
let Some((first, rest)) = inputs.split_first() else {
return Err(Error::invalid_input(
"Concatenate takes 1 input at least, and it received none",
));
};
let rank = first.rank();
let axis = self.resolve_axis(rank)?;
let mut merged = first.clone();
for input in rest {
if input.rank() != rank {
return Err(Error::invalid_input(format!(
"Concatenate joins inputs of 1 rank. The input shape {merged} holds rank \
{rank}, and the input shape {input} holds rank {}",
input.rank()
)));
}
let mut axes = Vec::with_capacity(rank);
for (position, (held, found)) in merged.axes().iter().zip(input.axes()).enumerate() {
let extent = if position == axis {
match (held, found) {
(Some(held), Some(found)) => Some(held + found),
_ => None,
}
} else {
match (held, found) {
(Some(held), Some(found)) if held != found => {
return Err(Error::invalid_input(format!(
"Concatenate joins on axis {axis}, so every other axis holds 1 \
extent. The input shape {merged} holds {held} on axis \
{position}, and the input shape {input} holds {found}"
)));
}
(Some(held), _) => Some(*held),
(None, found) => *found,
}
};
axes.push(extent);
}
merged = Shape::new(axes);
}
Ok(merged)
}
}
struct ConcatenateCache {
bands: Vec<usize>,
output: Vec<usize>,
}
impl LayerBase for Concatenate {
fn layer_type(&self) -> &str {
"Concatenate"
}
merge_layer_base_functions!();
}
impl Layer for Concatenate {
fn arity(&self) -> Arity {
Arity::AtLeast(1)
}
fn build_many(&mut self, inputs: &[Shape]) -> Result<(), Error> {
Arity::AtLeast(1).check("Concatenate", inputs.len())?;
let Some(shapes) = start_build_many(&self.built, "Concatenate", inputs)? else {
return Ok(());
};
self.joined_shape(&shapes)?;
self.built = Some(shapes);
Ok(())
}
fn compute_output_shape_many(&self, inputs: &[Shape]) -> Result<Shape, Error> {
Arity::AtLeast(1).check("Concatenate", inputs.len())?;
self.joined_shape(inputs)
}
fn forward_many(&self, inputs: &[&Tensor], ctx: &mut Ctx) -> Result<Tensor, Error> {
Arity::AtLeast(1).check("Concatenate", inputs.len())?;
let shapes: Vec<Shape> = inputs
.iter()
.map(|input| Shape::known(input.shape()))
.collect();
let joined = self.joined_shape(&shapes)?;
let Some(dims) = joined.dims() else {
return Err(Error::computation(format!(
"Concatenate joined tensors of fixed extents into the shape {joined}, which \
leaves an axis free"
)));
};
let axis = self.resolve_axis(dims.len())?;
let mut output = Tensor::zeros(dims.as_slice());
let mut bands = Vec::with_capacity(inputs.len());
let mut start = 0;
for &input in inputs {
let extent = input.shape()[axis];
output
.slice_axis_mut(Axis(axis), Slice::from(start..start + extent))
.assign(input);
start += extent;
bands.push(extent);
}
if ctx.is_training() {
ctx.push_cache(
"Concatenate",
ConcatenateCache {
bands,
output: dims,
},
);
}
Ok(output)
}
fn backward_many(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Vec<Tensor>, Error> {
let cache: ConcatenateCache = ctx.pop_cache("Concatenate")?;
if grad_output.shape() != cache.output.as_slice() {
return Err(Error::shape_mismatch(cache.output, grad_output.shape()));
}
let axis = self.resolve_axis(cache.output.len())?;
let mut grads = Vec::with_capacity(cache.bands.len());
let mut start = 0;
for extent in cache.bands {
let band = grad_output.slice_axis(Axis(axis), Slice::from(start..start + extent));
let mut owned = Tensor::zeros(band.raw_dim());
owned.assign(&band);
grads.push(owned);
start += extent;
}
Ok(grads)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::IxDyn;
fn tensor(dims: &[usize], values: &[f32]) -> Tensor {
Tensor::from_shape_vec(IxDyn(dims), values.to_vec()).unwrap()
}
#[test]
fn the_forward_pass_joins_along_the_last_axis() {
let left = tensor(&[2, 2], &[1.0, 2.0, 3.0, 4.0]);
let middle = tensor(&[2, 1], &[5.0, 6.0]);
let right = tensor(&[2, 2], &[7.0, 8.0, 9.0, 10.0]);
let mut layer = Concatenate::new(-1);
let mut ctx = Ctx::training();
let output = layer
.forward_many_mut(&[&left, &middle, &right], &mut ctx)
.unwrap();
assert_eq!(
output,
tensor(
&[2, 5],
&[1.0, 2.0, 5.0, 7.0, 8.0, 3.0, 4.0, 6.0, 9.0, 10.0]
)
);
assert!(output.is_standard_layout(), "a layer emits the C order");
}
#[test]
fn the_forward_pass_joins_along_the_batch_axis() {
let left = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let right = tensor(&[1, 3], &[7.0, 8.0, 9.0]);
let mut layer = Concatenate::new(0);
let mut ctx = Ctx::inference();
let output = layer.forward_many_mut(&[&left, &right], &mut ctx).unwrap();
assert_eq!(
output,
tensor(&[3, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
);
assert_eq!(ctx.pending_caches(), 0, "an inference pass parks no cache");
}
#[test]
fn one_input_alone_comes_back_unchanged() {
let only = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut layer = Concatenate::new(1);
let mut ctx = Ctx::training();
let output = layer.forward_many_mut(&[&only], &mut ctx).unwrap();
assert_eq!(output, only);
let grads = layer.backward_many(&only, &mut ctx).unwrap();
assert_eq!(grads.len(), 1);
assert_eq!(grads[0], only);
}
#[test]
fn the_backward_pass_slices_the_gradient_in_input_order() {
let left = tensor(&[2, 2], &[1.0, 2.0, 3.0, 4.0]);
let middle = tensor(&[2, 1], &[5.0, 6.0]);
let right = tensor(&[2, 2], &[7.0, 8.0, 9.0, 10.0]);
let mut layer = Concatenate::new(-1);
let mut ctx = Ctx::training();
layer
.forward_many_mut(&[&left, &middle, &right], &mut ctx)
.unwrap();
let grad = tensor(
&[2, 5],
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],
);
let grads = layer.backward_many(&grad, &mut ctx).unwrap();
assert_eq!(grads.len(), 3);
assert_eq!(grads[0], tensor(&[2, 2], &[1.0, 2.0, 6.0, 7.0]));
assert_eq!(grads[1], tensor(&[2, 1], &[3.0, 8.0]));
assert_eq!(grads[2], tensor(&[2, 2], &[4.0, 5.0, 9.0, 10.0]));
for one in &grads {
assert!(one.is_standard_layout(), "a layer emits the C order");
}
assert_eq!(ctx.pending_caches(), 0, "the backward pass takes the cache");
}
#[test]
fn the_backward_pass_slices_the_batch_axis() {
let left = tensor(&[2, 2], &[1.0, 2.0, 3.0, 4.0]);
let right = tensor(&[1, 2], &[5.0, 6.0]);
let mut layer = Concatenate::new(0);
let mut ctx = Ctx::training();
layer.forward_many_mut(&[&left, &right], &mut ctx).unwrap();
let grad = tensor(&[3, 2], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let grads = layer.backward_many(&grad, &mut ctx).unwrap();
assert_eq!(grads[0], tensor(&[2, 2], &[1.0, 2.0, 3.0, 4.0]));
assert_eq!(grads[1], tensor(&[1, 2], &[5.0, 6.0]));
}
#[test]
fn no_axis_broadcasts() {
let layer = Concatenate::new(0);
assert_eq!(
layer
.compute_output_shape_many(&[Shape::known(&[2, 3]), Shape::known(&[1, 3])])
.unwrap(),
Shape::known(&[3, 3]),
"the batch axis joins like any other axis"
);
let message = layer
.compute_output_shape_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 1])])
.unwrap_err()
.to_string();
assert!(message.contains("Concatenate"), "{message}");
assert!(message.contains("(2, 3)"), "{message}");
assert!(message.contains("(2, 1)"), "{message}");
assert!(message.contains("axis 1"), "{message}");
let wide = tensor(&[2, 3], &[1.0; 6]);
let column = tensor(&[2, 1], &[1.0; 2]);
let mut ctx = Ctx::training();
assert!(
Concatenate::new(0)
.forward_many_mut(&[&wide, &column], &mut ctx)
.is_err()
);
assert_eq!(ctx.pending_caches(), 0, "a refused pass parks no cache");
}
#[test]
fn two_ranks_that_differ_are_refused() {
let mut layer = Concatenate::new(-1);
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 3, 4])])
.unwrap_err()
.to_string();
assert!(message.contains("Concatenate"), "{message}");
assert!(message.contains("rank 2"), "{message}");
assert!(message.contains("(2, 3, 4)"), "{message}");
assert!(!layer.is_built(), "a refused build records nothing");
}
#[test]
fn an_axis_outside_the_rank_is_refused() {
let layer = Concatenate::new(2);
let message = layer
.compute_output_shape_many(&[Shape::known(&[2, 3])])
.unwrap_err()
.to_string();
assert!(message.contains("axis 2"), "{message}");
assert!(message.contains("rank 2"), "{message}");
let layer = Concatenate::new(-3);
assert!(
layer
.compute_output_shape_many(&[Shape::known(&[2, 3])])
.is_err()
);
let layer = Concatenate::new(0);
assert!(
layer
.compute_output_shape_many(&[Shape::new(Vec::new())])
.is_err()
);
}
#[test]
fn a_negative_axis_counts_from_the_end() {
let layer = Concatenate::new(-2);
assert_eq!(
layer
.compute_output_shape_many(&[Shape::known(&[2, 3, 4]), Shape::known(&[2, 5, 4])])
.unwrap(),
Shape::known(&[2, 8, 4])
);
let layer = Concatenate::new(-3);
assert_eq!(
layer
.compute_output_shape_many(&[Shape::known(&[2, 3, 4]), Shape::known(&[5, 3, 4])])
.unwrap(),
Shape::known(&[7, 3, 4])
);
}
#[test]
fn a_free_extent_on_the_joined_axis_frees_the_output() {
let layer = Concatenate::new(-1);
let free = Shape::new(vec![Some(2), None]);
assert_eq!(
layer
.compute_output_shape_many(&[free, Shape::known(&[2, 3])])
.unwrap(),
Shape::new(vec![Some(2), None])
);
}
#[test]
fn a_free_axis_agrees_with_any_extent() {
let layer = Concatenate::new(0);
assert_eq!(
layer
.compute_output_shape_many(&[
Shape::with_free_batch(&[2, 3]),
Shape::known(&[4, 3]),
])
.unwrap(),
Shape::new(vec![None, Some(3)]),
"1 free batch extent leaves the joined axis free"
);
let layer = Concatenate::new(-1);
assert_eq!(
layer
.compute_output_shape_many(&[
Shape::with_free_batch(&[2, 3]),
Shape::known(&[4, 5]),
])
.unwrap(),
Shape::known(&[4, 8]),
"the free batch axis takes the fixed extent of the other side"
);
}
#[test]
fn the_layer_takes_one_input_at_least() {
let mut layer = Concatenate::new(-1);
assert_eq!(layer.arity(), Arity::AtLeast(1));
let message = layer.build_many(&[]).unwrap_err().to_string();
assert!(message.contains("Concatenate"), "{message}");
assert!(message.contains("1 or more"), "{message}");
assert!(layer.compute_output_shape_many(&[]).is_err());
let mut ctx = Ctx::training();
assert!(layer.forward_many(&[], &mut ctx).is_err());
assert_eq!(ctx.pending_caches(), 0, "a refused pass parks no cache");
}
#[test]
fn a_second_build_for_other_shapes_is_refused() {
let mut layer = Concatenate::new(-1);
layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 4])])
.unwrap();
layer
.build_many(&[Shape::known(&[8, 3]), Shape::known(&[8, 4])])
.unwrap();
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 5])])
.unwrap_err()
.to_string();
assert!(message.contains("already built"), "{message}");
}
#[test]
fn a_gradient_of_another_shape_is_refused() {
let left = tensor(&[2, 2], &[1.0; 4]);
let right = tensor(&[2, 1], &[1.0; 2]);
let mut layer = Concatenate::new(-1);
let mut ctx = Ctx::training();
layer.forward_many_mut(&[&left, &right], &mut ctx).unwrap();
let message = layer
.backward_many(&tensor(&[2, 4], &[1.0; 8]), &mut ctx)
.unwrap_err()
.to_string();
assert!(message.contains("[2, 3]"), "{message}");
assert!(message.contains("[2, 4]"), "{message}");
}
#[test]
fn the_layer_holds_no_array_and_reports_every_build_shape() {
let mut layer = Concatenate::new(-1);
assert_eq!(layer.param_count(), ParamCounts::none());
assert!(layer.weights().is_empty());
assert!(layer.weights_mut().is_empty());
assert!(!layer.is_built());
assert!(layer.build_config().is_none());
assert_eq!(layer.output_shape(), "Unknown");
layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 4])])
.unwrap();
assert!(layer.is_built());
assert_eq!(
layer.known_input_shapes(),
Some(vec![
Shape::with_free_batch(&[2, 3]),
Shape::with_free_batch(&[2, 4]),
]),
"every reported shape carries a free batch axis"
);
assert_eq!(layer.output_shape(), "(None, 7)");
assert_eq!(
layer.build_config().unwrap().input_shapes,
vec![
Shape::with_free_batch(&[2, 3]),
Shape::with_free_batch(&[2, 4]),
]
);
}
}