use onnx_ir_derive::NodeBuilder;
use crate::ir::{ArgType, Argument, DType, Node, RawNode, TensorDataExt, TensorType};
use crate::processor::{
ArgPreference, InputPreferences, InputSpec, NodeProcessor, NodeSpec, OutputPreferences,
OutputSpec, ProcessError, validate_opset,
};
use crate::proto_conversion::element_type_from_proto;
const OP_NAME: &str = "MelWeightMatrix";
#[derive(Debug, Clone)]
pub struct MelWeightMatrixConfig {
pub output_dtype: DType,
}
impl Default for MelWeightMatrixConfig {
fn default() -> Self {
Self {
output_dtype: DType::F32,
}
}
}
#[derive(Debug, Clone, NodeBuilder)]
pub struct MelWeightMatrixNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: MelWeightMatrixConfig,
}
pub(crate) struct MelWeightMatrixProcessor;
impl MelWeightMatrixProcessor {
fn resolve_output_dtype(node: &RawNode) -> Result<DType, ProcessError> {
let dtype = match node.attrs.get("output_datatype") {
Some(val) => {
let dt_i32 = val.clone().into_i32();
element_type_from_proto(dt_i32).map_err(|e| ProcessError::InvalidAttribute {
name: "output_datatype".to_string(),
reason: format!("{OP_NAME}: {e}"),
})?
}
None => DType::F32,
};
if !matches!(dtype, DType::F16 | DType::BF16 | DType::F32 | DType::F64) {
return Err(ProcessError::InvalidAttribute {
name: "output_datatype".to_string(),
reason: format!("{OP_NAME}: must be a float type, got {dtype:?}"),
});
}
Ok(dtype)
}
fn validate_scalar(
arg: &Argument,
name: &str,
allowed: &[DType],
dtype_label: &str,
) -> Result<(), ProcessError> {
let is_scalar_shape = arg.ty.is_scalar()
|| matches!(&arg.ty, ArgType::Tensor(t) if t.rank == 0)
|| matches!(&arg.ty, ArgType::Tensor(t) if t.rank == 1
&& t.static_shape.as_ref().is_some_and(|s| s == &[Some(1)]));
if !is_scalar_shape {
return Err(ProcessError::TypeMismatch {
expected: format!("scalar {name}"),
actual: format!("{:?}", arg.ty),
});
}
let dtype = arg.ty.elem_type();
if !allowed.contains(&dtype) {
return Err(ProcessError::TypeMismatch {
expected: format!("{dtype_label} {name}"),
actual: format!("{dtype:?}"),
});
}
Ok(())
}
fn validate_int_scalar(arg: &Argument, name: &str) -> Result<(), ProcessError> {
Self::validate_scalar(arg, name, &[DType::I32, DType::I64], "int32/int64")
}
fn validate_float_scalar(arg: &Argument, name: &str) -> Result<(), ProcessError> {
Self::validate_scalar(arg, name, &[DType::F32], "float32")
}
}
impl NodeProcessor for MelWeightMatrixProcessor {
type Config = MelWeightMatrixConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 17,
max_opset: None,
inputs: InputSpec::Exact(5),
outputs: OutputSpec::Exact(1),
}
}
fn input_preferences(
&self,
node: &RawNode,
_opset: usize,
) -> Result<Option<InputPreferences>, ProcessError> {
let mut prefs = InputPreferences::new();
for input in node.inputs.iter().take(5) {
prefs = prefs.add(&input.name, ArgPreference::ScalarNative);
}
Ok(Some(prefs))
}
fn infer_types(
&self,
node: &mut RawNode,
opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
validate_opset(opset, 17)?;
Self::validate_int_scalar(&node.inputs[0], "num_mel_bins")?;
Self::validate_int_scalar(&node.inputs[1], "dft_length")?;
Self::validate_int_scalar(&node.inputs[2], "sample_rate")?;
Self::validate_float_scalar(&node.inputs[3], "lower_edge_hertz")?;
Self::validate_float_scalar(&node.inputs[4], "upper_edge_hertz")?;
let output_dtype = Self::resolve_output_dtype(node)?;
if let (Some(lower_data), Some(upper_data)) =
(node.inputs[3].value(), node.inputs[4].value())
&& let (Ok(lower), Ok(upper)) = (lower_data.scalar_f64(), upper_data.scalar_f64())
&& lower >= upper
{
return Err(ProcessError::Custom(format!(
"{OP_NAME}: lower_edge_hertz ({lower}) must be strictly less than \
upper_edge_hertz ({upper})"
)));
}
let num_mel_bins = node.inputs[0]
.value()
.and_then(|d| d.scalar_i64().ok())
.filter(|&v| v >= 0)
.map(|v| v as usize);
let dft_length = node.inputs[1]
.value()
.and_then(|d| d.scalar_i64().ok())
.filter(|&v| v >= 0)
.map(|v| v as usize);
let static_shape = match (dft_length, num_mel_bins) {
(Some(dft), Some(mel)) => Some(vec![Some(dft / 2 + 1), Some(mel)]),
_ => None,
};
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: output_dtype,
rank: 2,
static_shape,
});
Ok(())
}
fn extract_config(&self, node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError> {
let output_dtype = Self::resolve_output_dtype(node)?;
Ok(MelWeightMatrixConfig { output_dtype })
}
fn build_node(&self, builder: RawNode, opset: usize) -> Node {
let config = self.extract_config(&builder, opset).unwrap_or_else(|e| {
panic!(
"{OP_NAME} ({}): config extraction failed: {e}",
builder.name
)
});
Node::MelWeightMatrix(MelWeightMatrixNode {
name: builder.name,
inputs: builder.inputs,
outputs: builder.outputs,
config,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::NodeType;
use crate::node::test_utils::TestNodeBuilder;
use crate::processor::OutputPreferences;
fn runtime_builder() -> TestNodeBuilder {
TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_i64("num_mel_bins", 0, None)
.input_tensor_i64("dft_length", 0, None)
.input_tensor_i64("sample_rate", 0, None)
.input_tensor_f32("lower_edge_hertz", 0, None)
.input_tensor_f32("upper_edge_hertz", 0, None)
.output_tensor_f32("output", 0, None)
}
#[test]
fn test_mwm_runtime_inputs() {
let mut node = runtime_builder().build();
let prefs = OutputPreferences::new();
MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(t) => {
assert_eq!(t.rank, 2);
assert_eq!(t.dtype, DType::F32);
assert_eq!(t.static_shape, None);
}
_ => panic!("expected Tensor output"),
}
}
#[test]
fn test_mwm_constant_inputs_static_shape() {
let mut node = TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_i64_data("num_mel_bins", vec![8], vec![])
.input_tensor_i64_data("dft_length", vec![16], vec![])
.input_tensor_i64_data("sample_rate", vec![16000], vec![])
.input_tensor_f32_data("lower_edge_hertz", vec![0.0], vec![])
.input_tensor_f32_data("upper_edge_hertz", vec![8000.0], vec![])
.output_tensor_f32("output", 0, None)
.build_with_graph_data(17);
let prefs = OutputPreferences::new();
MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(t) => {
assert_eq!(t.rank, 2);
assert_eq!(t.static_shape, Some(vec![Some(9), Some(8)]));
}
_ => panic!("expected Tensor output"),
}
}
#[test]
fn test_mwm_output_dtype_f64() {
let mut node = runtime_builder()
.attr_int("output_datatype", 11) .build();
let prefs = OutputPreferences::new();
MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(t) => assert_eq!(t.dtype, DType::F64),
_ => panic!("expected Tensor output"),
}
}
#[test]
fn test_mwm_rejects_integer_output_dtype() {
let mut node = runtime_builder()
.attr_int("output_datatype", 7) .build();
let prefs = OutputPreferences::new();
let err = MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap_err();
assert!(matches!(err, ProcessError::InvalidAttribute { .. }));
}
#[test]
fn test_mwm_rejects_float_num_mel_bins() {
let mut node = TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_f32("num_mel_bins", 0, None)
.input_tensor_i64("dft_length", 0, None)
.input_tensor_i64("sample_rate", 0, None)
.input_tensor_f32("lower_edge_hertz", 0, None)
.input_tensor_f32("upper_edge_hertz", 0, None)
.output_tensor_f32("output", 0, None)
.build();
let prefs = OutputPreferences::new();
let err = MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap_err();
assert!(matches!(err, ProcessError::TypeMismatch { .. }));
}
#[test]
fn test_mwm_rejects_integer_lower_edge_hertz() {
let mut node = TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_i64("num_mel_bins", 0, None)
.input_tensor_i64("dft_length", 0, None)
.input_tensor_i64("sample_rate", 0, None)
.input_tensor_i64("lower_edge_hertz", 0, None)
.input_tensor_f32("upper_edge_hertz", 0, None)
.output_tensor_f32("output", 0, None)
.build();
let prefs = OutputPreferences::new();
let err = MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap_err();
assert!(matches!(err, ProcessError::TypeMismatch { .. }));
}
#[test]
fn test_mwm_rejects_f64_edge_hertz() {
let mut node = TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_i64("num_mel_bins", 0, None)
.input_tensor_i64("dft_length", 0, None)
.input_tensor_i64("sample_rate", 0, None)
.input_tensor_f64("lower_edge_hertz", 0, None)
.input_tensor_f32("upper_edge_hertz", 0, None)
.output_tensor_f32("output", 0, None)
.build();
let prefs = OutputPreferences::new();
let err = MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap_err();
assert!(matches!(err, ProcessError::TypeMismatch { .. }));
}
#[test]
fn test_mwm_opset_too_low() {
let mut node = runtime_builder().build();
let prefs = OutputPreferences::new();
let err = MelWeightMatrixProcessor
.infer_types(&mut node, 16, &prefs)
.unwrap_err();
assert!(err.to_string().contains("opset"));
}
#[test]
fn test_mwm_rejects_lower_ge_upper_edge() {
let mut node = TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_i64_data("num_mel_bins", vec![8], vec![])
.input_tensor_i64_data("dft_length", vec![16], vec![])
.input_tensor_i64_data("sample_rate", vec![16000], vec![])
.input_tensor_f32_data("lower_edge_hertz", vec![4000.0], vec![])
.input_tensor_f32_data("upper_edge_hertz", vec![100.0], vec![])
.output_tensor_f32("output", 0, None)
.build_with_graph_data(17);
let prefs = OutputPreferences::new();
let err = MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap_err();
assert!(
err.to_string().contains("strictly less than"),
"expected lower<upper error, got: {err}"
);
}
#[test]
fn test_mwm_accepts_i32_int_inputs() {
let mut node = TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_i32("num_mel_bins", 0, None)
.input_tensor_i32("dft_length", 0, None)
.input_tensor_i32("sample_rate", 0, None)
.input_tensor_f32("lower_edge_hertz", 0, None)
.input_tensor_f32("upper_edge_hertz", 0, None)
.output_tensor_f32("output", 0, None)
.build();
let prefs = OutputPreferences::new();
MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap();
}
#[test]
fn test_mwm_accepts_rank1_singleton_scalar() {
let mut node = TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_i64("num_mel_bins", 1, Some(vec![1]))
.input_tensor_i64("dft_length", 1, Some(vec![1]))
.input_tensor_i64("sample_rate", 1, Some(vec![1]))
.input_tensor_f32("lower_edge_hertz", 1, Some(vec![1]))
.input_tensor_f32("upper_edge_hertz", 1, Some(vec![1]))
.output_tensor_f32("output", 0, None)
.build();
let prefs = OutputPreferences::new();
MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap();
}
#[test]
fn test_mwm_partial_constant_shape_is_dynamic() {
let mut node = TestNodeBuilder::new(NodeType::MelWeightMatrix, "test_mwm")
.input_tensor_i64_data("num_mel_bins", vec![8], vec![])
.input_tensor_i64("dft_length", 0, None)
.input_tensor_i64("sample_rate", 0, None)
.input_tensor_f32("lower_edge_hertz", 0, None)
.input_tensor_f32("upper_edge_hertz", 0, None)
.output_tensor_f32("output", 0, None)
.build_with_graph_data(17);
let prefs = OutputPreferences::new();
MelWeightMatrixProcessor
.infer_types(&mut node, 17, &prefs)
.unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(t) => {
assert_eq!(t.static_shape, None);
}
_ => panic!("expected Tensor output"),
}
}
}