use fuel_ir::error::Result;
use fuel_ir::{DType, Shape};
use fuel_tensor::lazy::Tensor;
use std::sync::Arc;
pub fn one_hot(
labels: &Tensor,
num_classes: usize,
on_value: f32,
off_value: f32,
) -> Result<Tensor> {
if labels.dtype() != DType::U32 {
return Err(fuel_ir::error::Error::Msg(format!(
"one_hot: labels must be U32, got {:?}",
labels.dtype(),
))
.bt());
}
if num_classes == 0 {
return Err(fuel_ir::error::Error::Msg("one_hot: num_classes must be ≥ 1".into()).bt());
}
let labels_shape = labels.shape();
let labels_dims: Vec<usize> = labels_shape.dims().to_vec();
let n: usize = labels_shape.elem_count();
let mut table: Vec<f32> = vec![off_value; num_classes * num_classes];
for i in 0..num_classes {
table[i * num_classes + i] = on_value;
}
let table_data: Arc<[f32]> = Arc::<[f32]>::from(table);
let table_t =
labels.const_f32_like(table_data, Shape::from_dims(&[num_classes, num_classes]))?;
let flat_labels = labels.reshape(Shape::from_dims(&[n]))?;
let picked = table_t.index_select(0_usize, &flat_labels)?;
let mut out_dims = labels_dims;
out_dims.push(num_classes);
picked.reshape(Shape::from_dims(&out_dims))
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_tensor::Device;
fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() <= eps
}
#[test]
fn one_hot_rank1_classic_1_0_pattern() {
let device = Device::cpu();
let labels = Tensor::from_u32(vec![0_u32, 2, 1], Shape::from_dims(&[3]), &device).unwrap();
let oh = one_hot(&labels, 3, 1.0, 0.0).unwrap();
assert_eq!(oh.shape().dims(), &[3, 3]);
let v = oh.realize_f32();
let expected = [1.0_f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0];
assert_eq!(v.len(), expected.len());
for (i, (g, e)) in v.iter().zip(expected.iter()).enumerate() {
assert!(
approx_eq(*g, *e, 1e-7),
"one_hot mismatch at {i}: got {g} expected {e}",
);
}
}
#[test]
fn one_hot_rank2_preserves_leading_dims_and_class_dim() {
let device = Device::cpu();
let labels =
Tensor::from_u32(vec![0_u32, 1, 2, 0], Shape::from_dims(&[2, 2]), &device).unwrap();
let oh = one_hot(&labels, 3, 1.0, 0.0).unwrap();
assert_eq!(oh.shape().dims(), &[2, 2, 3]);
let v = oh.realize_f32();
let expected = [
1.0_f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0,
];
assert_eq!(v.len(), expected.len());
for (i, (g, e)) in v.iter().zip(expected.iter()).enumerate() {
assert!(
approx_eq(*g, *e, 1e-7),
"one_hot mismatch at {i}: got {g} expected {e}",
);
}
}
#[test]
fn one_hot_one_cold_pattern_inverts_on_off() {
let device = Device::cpu();
let labels = Tensor::from_u32(vec![1_u32, 0], Shape::from_dims(&[2]), &device).unwrap();
let oc = one_hot(&labels, 3, 0.0, 1.0).unwrap();
assert_eq!(oc.shape().dims(), &[2, 3]);
let v = oc.realize_f32();
let expected = [1.0_f32, 0.0, 1.0, 0.0, 1.0, 1.0];
assert_eq!(v.len(), expected.len());
for (i, (g, e)) in v.iter().zip(expected.iter()).enumerate() {
assert!(
approx_eq(*g, *e, 1e-7),
"one_cold mismatch at {i}: got {g} expected {e}",
);
}
}
#[test]
fn one_hot_rejects_non_u32_labels() {
let device = Device::cpu();
let probe = Tensor::from_u32(vec![0_u32], Shape::from_dims(&[1]), &device).unwrap();
let bad = probe
.const_i64_like(vec![0_i64], Shape::from_dims(&[1]))
.unwrap();
let err = one_hot(&bad, 3, 1.0, 0.0);
assert!(err.is_err(), "one_hot should reject non-U32 labels");
}
#[test]
fn one_hot_rejects_zero_num_classes() {
let device = Device::cpu();
let labels = Tensor::from_u32(vec![0_u32], Shape::from_dims(&[1]), &device).unwrap();
let err = one_hot(&labels, 0, 1.0, 0.0);
assert!(err.is_err(), "one_hot should reject num_classes == 0");
}
}