use derive_new::new;
use onnx_ir_derive::NodeBuilder;
use crate::ir::Argument;
use crate::ir::{Node, RawNode, RuntimeInputRef, TensorDataExt};
use crate::processor::{
InputSpec, NodeProcessor, NodeSpec, OutputPreferences, OutputSpec, ProcessError, same_as_input,
};
#[derive(Debug, Clone)]
pub enum ClipInput {
Static(f64),
Runtime(RuntimeInputRef),
}
#[derive(Debug, Clone, new)]
pub struct ClipConfig {
pub min: Option<ClipInput>,
pub max: Option<ClipInput>,
}
#[derive(Debug, Clone, NodeBuilder)]
pub struct ClipNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: ClipConfig,
}
pub(crate) struct ClipProcessor;
impl NodeProcessor for ClipProcessor {
type Config = ClipConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 1,
max_opset: None,
inputs: InputSpec::AtLeast(1),
outputs: OutputSpec::Exact(1),
}
}
fn lift_constants(&self, node: &mut RawNode, _opset: usize) -> Result<(), ProcessError> {
if node.inputs.len() > 1 && node.inputs[1].is_constant() {
node.inputs[1].to_static()?;
}
if node.inputs.len() > 2 && node.inputs[2].is_constant() {
node.inputs[2].to_static()?;
}
Ok(())
}
fn infer_types(
&self,
node: &mut RawNode,
_opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
same_as_input(node);
Ok(())
}
fn extract_config(&self, node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError> {
fn get_clip_input(node: &RawNode, index: usize, _param_name: &str) -> Option<ClipInput> {
let input = node.inputs.get(index)?;
if input.is_optional() {
return None;
}
match input.value() {
None => {
Some(ClipInput::Runtime(RuntimeInputRef::new(
input.name.clone(),
index,
)))
}
Some(tensor_data) => {
match tensor_data.scalar_f64() {
Ok(value) => Some(ClipInput::Static(value)),
Err(_) => None, }
}
}
}
let mut min_result: Option<ClipInput> = None;
let mut max_result: Option<ClipInput> = None;
for (key, value) in node.attrs.iter() {
match key.as_str() {
"min" => {
let min = value.clone().into_f32() as f64;
min_result = Some(ClipInput::Static(min));
}
"max" => {
let max = value.clone().into_f32() as f64;
max_result = Some(ClipInput::Static(max));
}
_ => {}
}
}
if min_result.is_none() {
min_result = get_clip_input(node, 1, "min");
}
if max_result.is_none() {
max_result = get_clip_input(node, 2, "max");
}
let config = ClipConfig {
min: min_result,
max: max_result,
};
Ok(config)
}
fn build_node(&self, builder: RawNode, opset: usize) -> Node {
let config = self
.extract_config(&builder, opset)
.expect("Config extraction failed");
Node::Clip(ClipNode {
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;
fn create_test_node_with_attributes(min: Option<f32>, max: Option<f32>) -> RawNode {
let mut builder = TestNodeBuilder::new(NodeType::Clip, "test_clip")
.input_tensor_f32("X", 4, None)
.output_tensor_f32("Y", 4, None);
if let Some(min_val) = min {
builder = builder.attr_float("min", min_val);
}
if let Some(max_val) = max {
builder = builder.attr_float("max", max_val);
}
builder.build()
}
fn create_test_node_with_inputs(min: Option<f32>, max: Option<f32>) -> TestNodeBuilder {
TestNodeBuilder::new(NodeType::Clip, "test_clip")
.input_tensor_f32("X", 4, None)
.input_scalar_tensor_f32("min", min)
.input_scalar_tensor_f32("max", max)
.output_tensor_f32("Y", 4, None)
}
#[test]
fn test_clip_config_with_attributes() {
let node = create_test_node_with_attributes(Some(-1.0), Some(1.0));
let mut node = node;
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(config.min, Some(ClipInput::Static(v)) if (v - (-1.0)).abs() < 1e-6));
assert!(matches!(config.max, Some(ClipInput::Static(v)) if (v - 1.0).abs() < 1e-6));
}
#[test]
fn test_clip_config_with_attributes_min_only() {
let node = create_test_node_with_attributes(Some(-1.0), None);
let mut node = node;
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(config.min, Some(ClipInput::Static(v)) if (v - (-1.0)).abs() < 1e-6));
assert!(config.max.is_none());
}
#[test]
fn test_clip_config_with_attributes_max_only() {
let node = create_test_node_with_attributes(None, Some(1.0));
let mut node = node;
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(config.min.is_none());
assert!(matches!(config.max, Some(ClipInput::Static(v)) if (v - 1.0).abs() < 1e-6));
}
#[test]
fn test_clip_config_with_inputs() {
let node = create_test_node_with_inputs(Some(-1.0), Some(1.0)).build_with_graph_data(16);
let mut node = node;
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(config.min, Some(ClipInput::Static(v)) if (v - (-1.0)).abs() < 1e-6));
assert!(matches!(config.max, Some(ClipInput::Static(v)) if (v - 1.0).abs() < 1e-6));
}
#[test]
fn test_clip_config_with_inputs_min_only() {
let node = create_test_node_with_inputs(Some(-1.0), None).build_with_graph_data(16);
let mut node = node;
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(config.min, Some(ClipInput::Static(v)) if (v - (-1.0)).abs() < 1e-6));
assert!(matches!(config.max, Some(ClipInput::Runtime(_))));
}
#[test]
fn test_clip_config_with_inputs_max_only() {
let node = create_test_node_with_inputs(None, Some(1.0)).build_with_graph_data(16);
let mut node = node;
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(config.min, Some(ClipInput::Runtime(_))));
assert!(matches!(config.max, Some(ClipInput::Static(v)) if (v - 1.0).abs() < 1e-6));
}
fn create_test_node_with_runtime_inputs() -> TestNodeBuilder {
TestNodeBuilder::new(NodeType::Clip, "test_clip")
.input_tensor_f32("X", 4, None)
.input_tensor_f32("min", 0, None) .input_tensor_f32("max", 0, None) .output_tensor_f32("Y", 4, None)
}
#[test]
fn test_clip_config_with_runtime_inputs() {
let node = create_test_node_with_runtime_inputs().build();
let mut node = node;
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(config.min, Some(ClipInput::Runtime(ref arg)) if arg.name == "min"));
assert!(matches!(config.max, Some(ClipInput::Runtime(ref arg)) if arg.name == "max"));
}
#[test]
fn test_clip_config_mixed_static_runtime() {
let builder = TestNodeBuilder::new(NodeType::Clip, "test_clip")
.input_tensor_f32("X", 4, None)
.input_scalar_tensor_f32("min", Some(-1.0)) .input_tensor_f32("max", 0, None) .output_tensor_f32("Y", 4, None);
let node = builder.build_with_graph_data(16);
let mut node = node;
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(matches!(config.min, Some(ClipInput::Static(v)) if (v - (-1.0)).abs() < 1e-6));
assert!(matches!(config.max, Some(ClipInput::Runtime(ref arg)) if arg.name == "max"));
}
#[test]
fn test_clip_config_no_min_max() {
let node = create_test_node_with_attributes(None, None);
let processor = ClipProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert!(config.min.is_none());
assert!(config.max.is_none());
}
#[test]
fn test_clip_lift_constants_with_attributes_only() {
let mut node = create_test_node_with_attributes(Some(-1.0), Some(1.0));
let processor = ClipProcessor;
processor.lift_constants(&mut node, 16).unwrap();
assert_eq!(node.inputs.len(), 1);
}
#[test]
fn test_clip_lift_constants_with_runtime_inputs() {
let mut node = create_test_node_with_runtime_inputs().build();
let processor = ClipProcessor;
assert!(!node.inputs[1].is_constant()); assert!(!node.inputs[2].is_constant());
processor.lift_constants(&mut node, 16).unwrap();
assert!(!node.inputs[1].is_static());
assert!(!node.inputs[2].is_static());
}
#[test]
fn test_clip_lift_constants_with_static_inputs() {
let mut node =
create_test_node_with_inputs(Some(-1.0), Some(1.0)).build_with_graph_data(16);
let processor = ClipProcessor;
assert!(node.inputs[1].is_constant()); assert!(node.inputs[2].is_constant());
processor.lift_constants(&mut node, 16).unwrap();
assert!(node.inputs[1].is_static());
assert!(node.inputs[2].is_static());
}
}