use ndarray::{Array, ArrayD, ArrayViewD, Axis, IxDyn};
use crate::configs::DimName;
use crate::schema::{self, padding_axes, squeeze_padding_dims, LogicalOutput, SchemaV2};
use crate::{dequantize_cpu_chunked, DecoderError, DecoderResult, Quantization};
use edgefirst_tensor::{TensorDyn, TensorMapTrait, TensorTrait};
#[derive(Debug, Clone)]
pub(crate) struct DecodeProgram {
merges: Vec<LogicalMerge>,
}
#[derive(Debug, Clone)]
enum LogicalMerge {
Direct {
#[allow(dead_code)] name: Option<String>,
shape: Vec<usize>,
padding_axes: Vec<usize>,
quant: Option<Quantization>,
},
ChannelConcat {
children: Vec<PhysicalBinding>,
logical_shape: Vec<usize>,
channel_axis: usize,
padding_axes: Vec<usize>,
},
}
#[derive(Debug, Clone)]
struct PhysicalBinding {
shape: Vec<usize>,
quant: Option<Quantization>,
}
impl DecodeProgram {
pub fn try_from_schema(schema: &SchemaV2) -> DecoderResult<Option<Self>> {
let needs_merge = schema
.outputs
.iter()
.any(|l| l.type_.is_some() && !l.outputs.is_empty());
if !needs_merge {
return Ok(None);
}
let merges = schema
.outputs
.iter()
.filter(|l| l.type_.is_some())
.map(plan_logical)
.collect::<DecoderResult<Vec<_>>>()?;
Ok(Some(Self { merges }))
}
pub fn execute(&self, inputs: &[&TensorDyn]) -> DecoderResult<Vec<ArrayD<f32>>> {
let mut used: Vec<usize> = Vec::new();
self.merges
.iter()
.map(|m| execute_merge(m, inputs, &mut used))
.collect()
}
}
fn plan_logical(logical: &LogicalOutput) -> DecoderResult<LogicalMerge> {
if logical.outputs.is_empty() {
let pad = padding_axes(&logical.dshape);
return Ok(LogicalMerge::Direct {
name: logical.name.clone(),
shape: logical.shape.clone(),
padding_axes: pad,
quant: logical
.quantization
.as_ref()
.map(schema_quant_to_runtime)
.transpose()?,
});
}
let first_has_stride = logical.outputs[0].stride.is_some();
if first_has_stride {
Err(DecoderError::NotSupported(format!(
"logical `{}` has per-scale (strided) children but the schema was \
not claimed by the per-scale subsystem; mixed per-scale + \
channel-sub-split schemas are not supported by the merge path",
logical.name.as_deref().unwrap_or("<anonymous>")
)))
} else {
plan_channel_concat(logical)
}
}
fn plan_channel_concat(logical: &LogicalOutput) -> DecoderResult<LogicalMerge> {
let children = logical
.outputs
.iter()
.map(physical_binding)
.collect::<DecoderResult<Vec<_>>>()?;
let channel_axis = channel_axis_in_logical(logical)?;
let pad = padding_axes(&logical.dshape);
let (squeezed_shape, _) = squeeze_padding_dims(logical.shape.clone(), logical.dshape.clone());
Ok(LogicalMerge::ChannelConcat {
children,
logical_shape: squeezed_shape,
channel_axis,
padding_axes: pad,
})
}
fn physical_binding(p: &schema::PhysicalOutput) -> DecoderResult<PhysicalBinding> {
let quant = p
.quantization
.as_ref()
.map(schema_quant_to_runtime)
.transpose()?;
Ok(PhysicalBinding {
shape: p.shape.clone(),
quant,
})
}
fn schema_quant_to_runtime(q: &schema::Quantization) -> DecoderResult<Quantization> {
if q.is_per_channel() {
return Err(DecoderError::NotSupported(format!(
"per-channel quantization (axis {:?}, {} scales) is not yet \
supported by the HAL merge path",
q.axis,
q.scale.len(),
)));
}
Ok(Quantization::new(
*q.scale.first().unwrap_or(&0.0),
q.zero_point_at(0),
))
}
fn channel_axis_in_logical(logical: &LogicalOutput) -> DecoderResult<usize> {
if !logical.dshape.is_empty() {
for (i, (name, _)) in logical.dshape.iter().enumerate() {
if matches!(
name,
DimName::BoxCoords
| DimName::NumFeatures
| DimName::NumClasses
| DimName::NumProtos
| DimName::NumAnchorsXFeatures
) {
return Ok(i);
}
}
}
Err(DecoderError::InvalidConfig(format!(
"logical `{}` has channel-sub-split children; `dshape` must name \
a channel axis (box_coords, num_features, num_classes, num_protos)",
logical.name.as_deref().unwrap_or("<anonymous>")
)))
}
fn execute_merge(
merge: &LogicalMerge,
inputs: &[&TensorDyn],
used: &mut Vec<usize>,
) -> DecoderResult<ArrayD<f32>> {
match merge {
LogicalMerge::Direct {
shape,
padding_axes,
quant,
..
} => {
let mut arr = find_and_dequantize(inputs, shape, *quant, used)?;
for &ax in padding_axes {
if ax < arr.ndim() && arr.shape()[ax] == 1 {
arr = arr.remove_axis(Axis(ax));
}
}
Ok(arr)
}
LogicalMerge::ChannelConcat {
children,
logical_shape,
channel_axis,
padding_axes,
} => execute_channel_concat(
inputs,
children,
logical_shape,
*channel_axis,
padding_axes,
used,
),
}
}
fn find_unused_tensor_by_shape<'a>(
inputs: &'a [&'a TensorDyn],
shape: &[usize],
used: &mut Vec<usize>,
) -> DecoderResult<&'a TensorDyn> {
for (i, t) in inputs.iter().enumerate() {
if used.contains(&i) {
continue;
}
if t.shape() == shape {
used.push(i);
return Ok(*t);
}
}
Err(DecoderError::InvalidShape(format!(
"no remaining input tensor matches shape {shape:?} (already \
bound tensors are excluded; pass inputs in schema child order, \
or use name-keyed decode once available)"
)))
}
fn find_and_dequantize(
inputs: &[&TensorDyn],
expected_shape: &[usize],
quant: Option<Quantization>,
used: &mut Vec<usize>,
) -> DecoderResult<ArrayD<f32>> {
if let Ok(t) = find_unused_tensor_by_shape(inputs, expected_shape, used) {
return tensor_to_f32(t, quant);
}
let expected_count: usize = expected_shape.iter().product();
for (i, t) in inputs.iter().enumerate() {
if used.contains(&i) {
continue;
}
let count: usize = t.shape().iter().product();
if count != expected_count {
continue;
}
let mut padded_shape = t.shape().to_vec();
while padded_shape.len() < expected_shape.len() {
padded_shape.push(1);
}
if let Some(perm) = find_axis_permutation(&padded_shape, expected_shape) {
used.push(i);
let arr = tensor_to_f32(t, quant)?;
let mut arr = if arr.ndim() < expected_shape.len() {
arr.into_shape_with_order(IxDyn(&padded_shape))
.map_err(DecoderError::NDArrayShape)?
} else {
arr
};
arr = arr.permuted_axes(IxDyn(&perm));
arr = arr.as_standard_layout().to_owned();
debug_assert_eq!(arr.shape(), expected_shape);
return Ok(arr);
}
}
Err(DecoderError::InvalidShape(format!(
"no remaining input tensor matches shape {expected_shape:?} \
(tried exact match and element-count + permutation; already \
bound tensors are excluded)"
)))
}
fn find_axis_permutation(from: &[usize], to: &[usize]) -> Option<Vec<usize>> {
if from.len() != to.len() {
return None;
}
let n = from.len();
let mut perm = vec![0usize; n];
let mut bound = vec![false; n];
for (i, &target_dim) in to.iter().enumerate() {
let mut found = false;
for (j, &source_dim) in from.iter().enumerate() {
if !bound[j] && source_dim == target_dim {
perm[i] = j;
bound[j] = true;
found = true;
break;
}
}
if !found {
return None;
}
}
Some(perm)
}
fn tensor_to_f32(t: &TensorDyn, quant: Option<Quantization>) -> DecoderResult<ArrayD<f32>> {
let shape = t.shape().to_vec();
match t {
TensorDyn::F16(tensor) => {
let m = tensor
.map()
.map_err(|e| DecoderError::Internal(format!("tensor map: {e}")))?;
use half::slice::HalfFloatSliceExt;
let total: usize = shape.iter().product();
let mut out = vec![0.0_f32; total];
m.as_slice().convert_to_f32_slice(&mut out);
Ok(Array::from_shape_vec(IxDyn(&shape), out)?)
}
TensorDyn::F32(tensor) => {
let m = tensor
.map()
.map_err(|e| DecoderError::Internal(format!("tensor map: {e}")))?;
let view = ArrayViewD::from_shape(IxDyn(&shape), m.as_slice())?;
Ok(view.to_owned())
}
TensorDyn::F64(tensor) => {
let m = tensor
.map()
.map_err(|e| DecoderError::Internal(format!("tensor map: {e}")))?;
let view = ArrayViewD::from_shape(IxDyn(&shape), m.as_slice())?;
Ok(view.mapv(|v| v as f32))
}
TensorDyn::U8(_)
| TensorDyn::I8(_)
| TensorDyn::U16(_)
| TensorDyn::I16(_)
| TensorDyn::U32(_)
| TensorDyn::I32(_) => dequantize_integer_tensor(t, quant, &shape),
other => Err(DecoderError::NotSupported(format!(
"merge: unsupported tensor dtype {:?}",
other.dtype()
))),
}
}
fn dequantize_integer_tensor(
t: &TensorDyn,
quant: Option<Quantization>,
shape: &[usize],
) -> DecoderResult<ArrayD<f32>> {
let quant = quant.unwrap_or(Quantization::new(1.0, 0));
let total: usize = shape.iter().product();
let mut out = vec![0.0_f32; total];
macro_rules! dq {
($tensor:expr) => {{
let m = $tensor
.map()
.map_err(|e| DecoderError::Internal(format!("tensor map: {e}")))?;
dequantize_cpu_chunked(m.as_slice(), quant, &mut out);
}};
}
match t {
TensorDyn::U8(tensor) => dq!(tensor),
TensorDyn::I8(tensor) => dq!(tensor),
TensorDyn::U16(tensor) => dq!(tensor),
TensorDyn::I16(tensor) => dq!(tensor),
TensorDyn::U32(tensor) => dq!(tensor),
TensorDyn::I32(tensor) => dq!(tensor),
_ => unreachable!("dequantize_integer_tensor called on non-integer dtype"),
}
let arr = Array::from_shape_vec(IxDyn(shape), out)?;
Ok(arr)
}
fn execute_channel_concat(
inputs: &[&TensorDyn],
children: &[PhysicalBinding],
logical_shape: &[usize],
channel_axis: usize,
padding_axes: &[usize],
used: &mut Vec<usize>,
) -> DecoderResult<ArrayD<f32>> {
let mut parts = Vec::with_capacity(children.len());
for child in children {
parts.push(find_and_dequantize(
inputs,
&child.shape,
child.quant,
used,
)?);
}
let views: Vec<_> = parts.iter().map(|a| a.view()).collect();
let mut merged =
ndarray::concatenate(Axis(channel_axis), &views).map_err(DecoderError::NDArrayShape)?;
for &ax in padding_axes {
if ax < merged.ndim() && merged.shape()[ax] == 1 {
merged = merged.remove_axis(Axis(ax));
}
}
if merged.shape() != logical_shape {
return Err(DecoderError::InvalidShape(format!(
"channel-concat produced shape {:?} but logical expected {:?}",
merged.shape(),
logical_shape
)));
}
Ok(merged)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::schema::{
BoxEncoding, DType, DecoderKind, LogicalOutput, LogicalType, PhysicalOutput, PhysicalType,
Quantization as SchemaQuant, SchemaV2, ScoreFormat,
};
use edgefirst_tensor::{Tensor, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait};
fn make_u8_tensor(shape: &[usize], values: &[u8]) -> TensorDyn {
let t = Tensor::<u8>::new(shape, Some(TensorMemory::Mem), None).unwrap();
let mut m = t.map().unwrap();
let slice = m.as_mut_slice();
slice[..values.len()].copy_from_slice(values);
drop(m);
TensorDyn::U8(t)
}
fn make_i16_tensor(shape: &[usize], values: &[i16]) -> TensorDyn {
let t = Tensor::<i16>::new(shape, Some(TensorMemory::Mem), None).unwrap();
let mut m = t.map().unwrap();
let slice = m.as_mut_slice();
slice[..values.len()].copy_from_slice(values);
drop(m);
TensorDyn::I16(t)
}
fn make_f32_tensor(shape: &[usize], values: &[f32]) -> TensorDyn {
let t = Tensor::<f32>::new(shape, Some(TensorMemory::Mem), None).unwrap();
let mut m = t.map().unwrap();
let slice = m.as_mut_slice();
slice[..values.len()].copy_from_slice(values);
drop(m);
TensorDyn::F32(t)
}
fn make_f16_tensor(shape: &[usize], values: &[f32]) -> TensorDyn {
let t = Tensor::<half::f16>::new(shape, Some(TensorMemory::Mem), None).unwrap();
let mut m = t.map().unwrap();
let slice = m.as_mut_slice();
for (dst, &src) in slice.iter_mut().zip(values.iter()) {
*dst = half::f16::from_f32(src);
}
drop(m);
TensorDyn::F16(t)
}
#[test]
fn tensor_to_f32_widens_f16_natively() {
let t = make_f16_tensor(&[2, 3], &[1.0, -2.0, 0.5, 0.25, -0.125, 4.0]);
let arr = super::tensor_to_f32(&t, None).unwrap();
assert_eq!(arr.shape(), &[2, 3]);
let flat: Vec<f32> = arr.iter().copied().collect();
assert_eq!(flat, vec![1.0, -2.0, 0.5, 0.25, -0.125, 4.0]);
}
fn per_tensor_q(scale: f32, zp: i32, dt: DType) -> SchemaQuant {
SchemaQuant {
scale: vec![scale],
zero_point: Some(vec![zp]),
axis: None,
dtype: Some(dt),
}
}
#[test]
fn typeless_logical_not_included_in_decode_program() {
let schema = SchemaV2 {
schema_version: 2,
outputs: vec![
LogicalOutput {
name: Some("user_custom".into()),
type_: None,
shape: vec![1, 32],
dshape: vec![],
decoder: None,
encoding: None,
score_format: None,
normalized: None,
anchors: None,
stride: None,
dtype: None,
quantization: None,
outputs: vec![],
activation_applied: None,
activation_required: None,
},
LogicalOutput {
name: Some("boxes".into()),
type_: Some(LogicalType::Boxes),
shape: vec![1, 4, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 4),
(DimName::NumBoxes, 3),
],
decoder: Some(DecoderKind::Ultralytics),
encoding: Some(BoxEncoding::Direct),
score_format: None,
normalized: Some(true),
anchors: None,
stride: None,
dtype: None,
quantization: None,
outputs: vec![PhysicalOutput {
name: "boxes_raw".into(),
type_: Some(PhysicalType::Boxes),
shape: vec![1, 4, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 4),
(DimName::NumBoxes, 3),
],
dtype: DType::Float32,
quantization: None,
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
}],
activation_applied: None,
activation_required: None,
},
],
..Default::default()
};
let program = DecodeProgram::try_from_schema(&schema)
.unwrap()
.expect("typed boxes has children → program must be Some");
let legacy = schema.to_legacy_config_outputs().unwrap();
assert_eq!(legacy.outputs.len(), 1);
let boxes_tensor = make_f32_tensor(&[1, 4, 3], &[1.0; 12]);
let inputs: Vec<&TensorDyn> = vec![&boxes_tensor];
let merged = program.execute(&inputs).unwrap();
assert_eq!(
merged.len(),
1,
"decode program must emit one tensor per typed logical, not \
per schema-order logical (would otherwise misalign with \
legacy ConfigOutputs passed to decode_float)"
);
}
#[test]
fn flat_schema_has_no_decode_program() {
let schema = SchemaV2 {
schema_version: 2,
outputs: vec![LogicalOutput {
name: Some("boxes".into()),
type_: Some(LogicalType::Boxes),
shape: vec![1, 4, 8400],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 4),
(DimName::NumBoxes, 8400),
],
decoder: Some(DecoderKind::Ultralytics),
encoding: Some(BoxEncoding::Direct),
score_format: None,
normalized: Some(true),
anchors: None,
stride: None,
dtype: Some(DType::Float32),
quantization: None,
outputs: vec![],
activation_applied: None,
activation_required: None,
}],
..Default::default()
};
let program = DecodeProgram::try_from_schema(&schema).unwrap();
assert!(program.is_none());
}
#[test]
fn channel_concat_merges_xy_and_wh_to_logical_shape() {
let boxes_logical = LogicalOutput {
name: Some("boxes".into()),
type_: Some(LogicalType::Boxes),
shape: vec![1, 4, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 4),
(DimName::NumBoxes, 3),
],
decoder: Some(DecoderKind::Ultralytics),
encoding: Some(BoxEncoding::Direct),
score_format: None,
normalized: Some(true),
anchors: None,
stride: None,
dtype: None,
quantization: None,
outputs: vec![
PhysicalOutput {
name: "xy".into(),
type_: Some(PhysicalType::BoxesXy),
shape: vec![1, 2, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 2),
(DimName::NumBoxes, 3),
],
dtype: DType::Int16,
quantization: Some(per_tensor_q(0.01, 0, DType::Int16)),
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
},
PhysicalOutput {
name: "wh".into(),
type_: Some(PhysicalType::BoxesWh),
shape: vec![1, 2, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 2),
(DimName::NumBoxes, 3),
],
dtype: DType::Int16,
quantization: Some(per_tensor_q(0.02, 0, DType::Int16)),
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
},
],
activation_applied: None,
activation_required: None,
};
let mut schema = SchemaV2::default();
schema.outputs.push(LogicalOutput {
shape: vec![1, 3, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 3),
(DimName::NumBoxes, 3),
],
outputs: vec![
PhysicalOutput {
name: "xy".into(),
type_: Some(PhysicalType::BoxesXy),
shape: vec![1, 1, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 1),
(DimName::NumBoxes, 3),
],
dtype: DType::Int16,
quantization: Some(per_tensor_q(0.01, 0, DType::Int16)),
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
},
PhysicalOutput {
name: "wh".into(),
type_: Some(PhysicalType::BoxesWh),
shape: vec![1, 2, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 2),
(DimName::NumBoxes, 3),
],
dtype: DType::Int16,
quantization: Some(per_tensor_q(0.02, 0, DType::Int16)),
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
},
],
..boxes_logical
});
let program = DecodeProgram::try_from_schema(&schema).unwrap().unwrap();
let xy = make_i16_tensor(&[1, 1, 3], &[100, 200, 300]);
let wh = make_i16_tensor(&[1, 2, 3], &[10, 20, 30, 40, 50, 60]);
let inputs: Vec<&TensorDyn> = vec![&xy, &wh];
let merged = program.execute(&inputs).unwrap();
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].shape(), &[1, 3, 3]);
let arr = &merged[0];
assert!((arr[[0, 0, 0]] - 1.0).abs() < 1e-5);
assert!((arr[[0, 0, 2]] - 3.0).abs() < 1e-5);
assert!((arr[[0, 1, 0]] - 0.2).abs() < 1e-5);
assert!((arr[[0, 2, 2]] - 1.2).abs() < 1e-5);
}
#[test]
fn direct_logical_with_float_tensor_pass_through() {
let schema = SchemaV2 {
schema_version: 2,
outputs: vec![
LogicalOutput {
name: Some("boxes".into()),
type_: Some(LogicalType::Boxes),
shape: vec![1, 4, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 4),
(DimName::NumBoxes, 3),
],
decoder: Some(DecoderKind::Ultralytics),
encoding: Some(BoxEncoding::Direct),
score_format: None,
normalized: Some(true),
anchors: None,
stride: None,
dtype: Some(DType::Float32),
quantization: None,
outputs: vec![],
activation_applied: None,
activation_required: None,
},
LogicalOutput {
name: Some("scores".into()),
type_: Some(LogicalType::Scores),
shape: vec![1, 2, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::NumClasses, 2),
(DimName::NumBoxes, 3),
],
decoder: Some(DecoderKind::Ultralytics),
encoding: None,
score_format: Some(ScoreFormat::PerClass),
normalized: None,
anchors: None,
stride: None,
dtype: None,
quantization: None,
outputs: vec![
PhysicalOutput {
name: "s0".into(),
type_: Some(PhysicalType::Scores),
shape: vec![1, 1, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::NumClasses, 1),
(DimName::NumBoxes, 3),
],
dtype: DType::Uint8,
quantization: Some(per_tensor_q(0.5, 0, DType::Uint8)),
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
},
PhysicalOutput {
name: "s1".into(),
type_: Some(PhysicalType::Scores),
shape: vec![1, 1, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::NumClasses, 1),
(DimName::NumBoxes, 3),
],
dtype: DType::Uint8,
quantization: Some(per_tensor_q(0.25, 0, DType::Uint8)),
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
},
],
activation_applied: None,
activation_required: None,
},
],
..Default::default()
};
let program = DecodeProgram::try_from_schema(&schema).unwrap().unwrap();
let boxes = make_f32_tensor(
&[1, 4, 3],
&[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2],
);
let s0 = make_u8_tensor(&[1, 1, 3], &[2, 4, 6]);
let s1 = make_u8_tensor(&[1, 1, 3], &[8, 16, 24]);
let inputs: Vec<&TensorDyn> = vec![&boxes, &s0, &s1];
let merged = program.execute(&inputs).unwrap();
assert_eq!(merged.len(), 2);
assert!((merged[0][[0, 0, 0]] - 0.1).abs() < 1e-6);
assert!((merged[0][[0, 3, 2]] - 1.2).abs() < 1e-6);
assert!((merged[1][[0, 0, 0]] - 1.0).abs() < 1e-6);
assert!((merged[1][[0, 0, 2]] - 3.0).abs() < 1e-6);
assert!((merged[1][[0, 1, 0]] - 2.0).abs() < 1e-6);
assert!((merged[1][[0, 1, 2]] - 6.0).abs() < 1e-6);
}
#[test]
fn dequantize_affine_reference_values_match_validator() {
let schema = SchemaV2 {
schema_version: 2,
outputs: vec![
LogicalOutput {
name: Some("scores".into()),
type_: Some(LogicalType::Scores),
shape: vec![1, 1, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::NumClasses, 1),
(DimName::NumBoxes, 3),
],
decoder: Some(DecoderKind::Ultralytics),
encoding: None,
score_format: Some(ScoreFormat::PerClass),
normalized: None,
anchors: None,
stride: None,
dtype: Some(DType::Uint8),
quantization: Some(per_tensor_q(0.130, 70, DType::Uint8)),
outputs: vec![],
activation_applied: None,
activation_required: None,
},
LogicalOutput {
name: Some("boxes".into()),
type_: Some(LogicalType::Boxes),
shape: vec![1, 4, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 4),
(DimName::NumBoxes, 3),
],
decoder: Some(DecoderKind::Ultralytics),
encoding: Some(BoxEncoding::Direct),
score_format: None,
normalized: Some(true),
anchors: None,
stride: None,
dtype: None,
quantization: None,
outputs: vec![
PhysicalOutput {
name: "b0".into(),
type_: Some(PhysicalType::BoxesXy),
shape: vec![1, 2, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 2),
(DimName::NumBoxes, 3),
],
dtype: DType::Float32,
quantization: None,
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
},
PhysicalOutput {
name: "b1".into(),
type_: Some(PhysicalType::BoxesWh),
shape: vec![1, 2, 3],
dshape: vec![
(DimName::Batch, 1),
(DimName::BoxCoords, 2),
(DimName::NumBoxes, 3),
],
dtype: DType::Float32,
quantization: None,
stride: None,
scale_index: None,
activation_applied: None,
activation_required: None,
},
],
activation_applied: None,
activation_required: None,
},
],
..Default::default()
};
let program = DecodeProgram::try_from_schema(&schema).unwrap().unwrap();
let scores = make_u8_tensor(&[1, 1, 3], &[0, 70, 255]);
let xy = make_f32_tensor(&[1, 2, 3], &[0.0f32; 6]);
let wh = make_f32_tensor(&[1, 2, 3], &[0.0f32; 6]);
let inputs: Vec<&TensorDyn> = vec![&scores, &xy, &wh];
let merged = program.execute(&inputs).unwrap();
let scores_out = &merged[0];
assert!(
(scores_out[[0, 0, 0]] - (-9.10)).abs() < 1e-4,
"{}",
scores_out[[0, 0, 0]]
);
assert!(
(scores_out[[0, 0, 1]] - 0.00).abs() < 1e-4,
"{}",
scores_out[[0, 0, 1]]
);
assert!(
(scores_out[[0, 0, 2]] - 24.05).abs() < 1e-3,
"{}",
scores_out[[0, 0, 2]]
);
}
#[test]
fn find_and_dequantize_exact_match_preferred() {
let t = make_f32_tensor(&[1, 3, 4], &[1.0; 12]);
let inputs: Vec<&TensorDyn> = vec![&t];
let mut used = Vec::new();
let arr = find_and_dequantize(&inputs, &[1, 3, 4], None, &mut used).unwrap();
assert_eq!(arr.shape(), &[1, 3, 4]);
assert_eq!(used, vec![0]);
}
#[test]
fn find_and_dequantize_permuted_nchw_to_nhwc() {
let t = make_f32_tensor(
&[1, 3, 2, 2],
&[
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
);
let inputs: Vec<&TensorDyn> = vec![&t];
let mut used = Vec::new();
let arr = find_and_dequantize(&inputs, &[1, 2, 2, 3], None, &mut used).unwrap();
assert_eq!(arr.shape(), &[1, 2, 2, 3]);
assert_eq!(arr[[0, 0, 0, 0]], 1.0);
assert_eq!(arr[[0, 0, 0, 1]], 5.0);
assert_eq!(arr[[0, 0, 0, 2]], 9.0);
assert_eq!(arr[[0, 0, 1, 0]], 2.0);
assert_eq!(arr[[0, 0, 1, 1]], 6.0);
assert_eq!(arr[[0, 0, 1, 2]], 10.0);
assert_eq!(arr[[0, 1, 0, 0]], 3.0);
assert_eq!(arr[[0, 1, 0, 1]], 7.0);
assert_eq!(arr[[0, 1, 0, 2]], 11.0);
}
#[test]
fn find_and_dequantize_stripped_trailing_unit_dim() {
let t = make_f32_tensor(&[1, 6], &[10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
let inputs: Vec<&TensorDyn> = vec![&t];
let mut used = Vec::new();
let arr = find_and_dequantize(&inputs, &[1, 6, 1], None, &mut used).unwrap();
assert_eq!(arr.shape(), &[1, 6, 1]);
assert_eq!(arr[[0, 0, 0]], 10.0);
assert_eq!(arr[[0, 5, 0]], 60.0);
}
#[test]
fn find_and_dequantize_skips_already_used() {
let t0 = make_f32_tensor(&[2, 3], &[0.0; 6]);
let t1 = make_f32_tensor(&[3, 2], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let inputs: Vec<&TensorDyn> = vec![&t0, &t1];
let mut used = vec![0]; let arr = find_and_dequantize(&inputs, &[2, 3], None, &mut used).unwrap();
assert_eq!(arr.shape(), &[2, 3]);
assert!(used.contains(&1));
assert_eq!(arr[[0, 0]], 1.0);
assert_eq!(arr[[0, 1]], 3.0);
assert_eq!(arr[[0, 2]], 5.0);
assert_eq!(arr[[1, 0]], 2.0);
assert_eq!(arr[[1, 1]], 4.0);
assert_eq!(arr[[1, 2]], 6.0);
}
#[test]
fn find_and_dequantize_no_match_returns_error() {
let t = make_f32_tensor(&[2, 5], &[0.0; 10]);
let inputs: Vec<&TensorDyn> = vec![&t];
let mut used = Vec::new();
let result = find_and_dequantize(&inputs, &[3, 4], None, &mut used);
assert!(result.is_err());
}
#[test]
fn find_axis_permutation_identity() {
let perm = find_axis_permutation(&[1, 3, 4], &[1, 3, 4]);
assert_eq!(perm, Some(vec![0, 1, 2]));
}
#[test]
fn find_axis_permutation_nchw_to_nhwc() {
let perm = find_axis_permutation(&[1, 3, 2, 2], &[1, 2, 2, 3]);
assert_eq!(perm, Some(vec![0, 2, 3, 1]));
}
#[test]
fn find_axis_permutation_no_match() {
assert_eq!(find_axis_permutation(&[1, 3, 4], &[1, 4, 5]), None);
}
#[test]
fn find_axis_permutation_different_lengths() {
assert_eq!(find_axis_permutation(&[1, 3], &[1, 3, 1]), None);
}
}