use tenferro_tensor::{ShapeMismatch, ShapeVec, ValidationError};
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_error_to_validation(error: BroadcastError) -> ValidationError {
match error {
BroadcastError::IncompatibleBinary { lhs, rhs } => ShapeMismatch::IncompatibleShapes {
lhs: lhs.into_iter().collect(),
rhs: rhs.into_iter().collect(),
}
.into(),
BroadcastError::IncompatibleInput { input, output } => ShapeMismatch::ExpectedActual {
expected: output.into_iter().collect(),
actual: input.into_iter().collect(),
}
.into(),
BroadcastError::RankTooLarge { input, output } => ValidationError::RankMismatch {
expected: output.len(),
actual: input.len(),
},
}
}
pub fn broadcast_in_dim_extent_error(
input: &[usize],
output: &[usize],
dims: &[usize],
) -> Option<BroadcastError> {
if input.len() > output.len() {
return Some(BroadcastError::RankTooLarge {
input: input.to_vec(),
output: output.to_vec(),
});
}
if dims.len() != input.len()
|| dims.iter().any(|&axis| axis >= output.len())
|| dims
.iter()
.enumerate()
.any(|(index, &axis)| dims[..index].contains(&axis))
{
return None;
}
input
.iter()
.zip(dims)
.find_map(|(&input_dim, &output_axis)| {
let output_dim = output[output_axis];
(input_dim != output_dim && input_dim != 1).then(|| BroadcastError::IncompatibleInput {
input: input.to_vec(),
output: output.to_vec(),
})
})
}
pub fn broadcast_shape(lhs: &[usize], rhs: &[usize]) -> Result<ShapeVec, BroadcastError> {
let rank = lhs.len().max(rhs.len());
let mut out = ShapeVec::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<ShapeVec, BroadcastError> {
let mut iter = shapes.into_iter();
let Some(first) = iter.next() else {
return Ok(ShapeVec::new());
};
let mut out = ShapeVec::from(first);
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())]
}
}