use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BroadcastInputPlan {
pub source_shape: Vec<usize>,
pub dims: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum BroadcastError {
#[error("cannot broadcast shapes {lhs:?} and {rhs:?}")]
IncompatibleBinary { lhs: Vec<usize>, rhs: Vec<usize> },
#[error("cannot broadcast shape {input:?} to {output:?}")]
IncompatibleInput {
input: Vec<usize>,
output: Vec<usize>,
},
#[error("cannot broadcast higher-rank shape {input:?} to {output:?}")]
RankTooLarge {
input: Vec<usize>,
output: Vec<usize>,
},
}
pub fn broadcast_shape(lhs: &[usize], rhs: &[usize]) -> Result<Vec<usize>, BroadcastError> {
let rank = lhs.len().max(rhs.len());
let mut out = Vec::with_capacity(rank);
for axis in 0..rank {
let lhs_dim = aligned_dim(lhs, rank, axis);
let rhs_dim = aligned_dim(rhs, rank, axis);
if lhs_dim == rhs_dim {
out.push(lhs_dim);
} else if lhs_dim == 1 {
out.push(rhs_dim);
} else if rhs_dim == 1 {
out.push(lhs_dim);
} else {
return Err(BroadcastError::IncompatibleBinary {
lhs: lhs.to_vec(),
rhs: rhs.to_vec(),
});
}
}
Ok(out)
}
pub fn broadcast_shapes<'a>(
shapes: impl IntoIterator<Item = &'a [usize]>,
) -> Result<Vec<usize>, BroadcastError> {
let mut iter = shapes.into_iter();
let Some(first) = iter.next() else {
return Ok(Vec::new());
};
let mut out = first.to_vec();
for shape in iter {
out = broadcast_shape(&out, shape)?;
}
Ok(out)
}
pub fn broadcast_input_plan(
input: &[usize],
output: &[usize],
) -> Result<BroadcastInputPlan, BroadcastError> {
if input.len() > output.len() {
return Err(BroadcastError::RankTooLarge {
input: input.to_vec(),
output: output.to_vec(),
});
}
let rank_diff = output.len() - input.len();
let mut source_shape = Vec::with_capacity(input.len());
let mut dims = Vec::with_capacity(input.len());
for (src_axis, &src_dim) in input.iter().enumerate() {
let dst_axis = src_axis + rank_diff;
let dst_dim = output[dst_axis];
if src_dim != dst_dim && src_dim != 1 {
return Err(BroadcastError::IncompatibleInput {
input: input.to_vec(),
output: output.to_vec(),
});
}
if src_dim == 1 && dst_dim != 1 {
continue;
}
source_shape.push(src_dim);
dims.push(dst_axis);
}
Ok(BroadcastInputPlan { source_shape, dims })
}
fn aligned_dim(shape: &[usize], output_rank: usize, output_axis: usize) -> usize {
if output_axis < output_rank - shape.len() {
1
} else {
shape[output_axis - (output_rank - shape.len())]
}
}