serde-onnx 0.1.0

Strongly typed Rust data structures for ONNX models, with export and import
Documentation
use super::graph::{Graph, Model};
use super::types::Dim;

#[derive(Debug, Clone, PartialEq)]
pub enum ValidationError {
    GraphMissingName,
    DuplicateOutput { node_index: usize, name: String },
    DuplicateInput { node_index: usize, name: String },
    DuplicateValueInfo { kind: &'static str, name: String },
    DuplicateInitializer { name: String },
    UndefinedValue { node_index: usize, name: String },
    Cycle { path: Vec<String> },
    MissingShape { kind: &'static str, name: String },
    MultipleGraphOutputsForValue { name: String },
}

impl core::fmt::Display for ValidationError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::GraphMissingName => write!(f, "graph has no name"),
            Self::DuplicateOutput { node_index, name } => {
                write!(
                    f,
                    "node {node_index} declares duplicate output name {name:?}"
                )
            }
            Self::DuplicateInput { node_index, name } => {
                write!(
                    f,
                    "node {node_index} declares duplicate input name {name:?}"
                )
            }
            Self::DuplicateValueInfo { kind, name } => {
                write!(f, "duplicate {kind} value info name {name:?}")
            }
            Self::DuplicateInitializer { name } => {
                write!(f, "duplicate initializer name {name:?}")
            }
            Self::UndefinedValue { node_index, name } => {
                write!(f, "node {node_index} references undefined value {name:?}")
            }
            Self::Cycle { path } => {
                write!(f, "graph contains a cycle: {}", path.join(" -> "))
            }
            Self::MissingShape { kind, name } => {
                write!(
                    f,
                    "{kind} {name:?} of the main graph has no type/shape (rank)"
                )
            }
            Self::MultipleGraphOutputsForValue { name } => {
                write!(f, "value {name:?} appears more than once in graph outputs")
            }
        }
    }
}

impl std::error::Error for ValidationError {}

#[derive(Debug, Clone, PartialEq)]
pub struct ValidationReport {
    pub errors: Vec<ValidationError>,
}

impl ValidationReport {
    pub const fn is_ok(&self) -> bool {
        self.errors.is_empty()
    }

    pub fn into_result(self) -> Result<(), Vec<ValidationError>> {
        if self.is_ok() {
            Ok(())
        } else {
            Err(self.errors)
        }
    }
}

impl core::fmt::Display for ValidationReport {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        for (i, e) in self.errors.iter().enumerate() {
            if i > 0 {
                writeln!(f)?;
            }
            write!(f, "{e}")?;
        }
        Ok(())
    }
}

impl std::error::Error for ValidationReport {}

pub fn validate_graph(graph: &Graph) -> ValidationReport {
    let mut errors = Vec::new();

    if graph.name.is_empty() {
        errors.push(ValidationError::GraphMissingName);
    }

    for vi in graph
        .inputs
        .iter()
        .chain(&graph.outputs)
        .chain(&graph.value_info)
    {
        if vi.name.is_empty() {
            errors.push(ValidationError::DuplicateValueInfo {
                kind: "unnamed",
                name: vi.name.clone(),
            });
        }
    }

    for (kind, list) in [
        ("input", &graph.inputs),
        ("output", &graph.outputs),
        ("value_info", &graph.value_info),
    ] {
        let mut seen = std::collections::HashSet::new();
        for vi in list {
            if !seen.insert(vi.name.clone()) {
                errors.push(ValidationError::DuplicateValueInfo {
                    kind,
                    name: vi.name.clone(),
                });
            }
        }
    }

    let mut init_names = std::collections::HashSet::new();
    for t in &graph.initializers {
        if !init_names.insert(t.name.clone()) {
            errors.push(ValidationError::DuplicateInitializer {
                name: t.name.clone(),
            });
        }
    }

    let mut defined: std::collections::HashSet<String> = init_names;
    for vi in &graph.inputs {
        defined.insert(vi.name.clone());
    }

    let mut produced_names: std::collections::HashSet<String> = std::collections::HashSet::new();
    for (idx, node) in graph.nodes.iter().enumerate() {
        for inp in &node.inputs {
            if inp.is_empty() {
                continue;
            }
            if !defined.contains(inp) {
                errors.push(ValidationError::UndefinedValue {
                    node_index: idx,
                    name: inp.clone(),
                });
            }
        }
        for out in &node.outputs {
            if out.is_empty() {
                continue;
            }
            if produced_names.contains(out) {
                errors.push(ValidationError::DuplicateOutput {
                    node_index: idx,
                    name: out.clone(),
                });
            } else {
                produced_names.insert(out.clone());
                defined.insert(out.clone());
            }
        }
    }

    let has_cycle = detect_cycle(graph);
    if let Some(path) = has_cycle {
        errors.push(ValidationError::Cycle { path });
    }

    for vi in &graph.outputs {
        let unshaped = vi
            .value_type
            .as_ref()
            .is_none_or(|vt| !value_type_has_rank(vt));
        if unshaped {
            errors.push(ValidationError::MissingShape {
                kind: "output",
                name: vi.name.clone(),
            });
        }
    }
    for vi in &graph.inputs {
        let unshaped = vi
            .value_type
            .as_ref()
            .is_none_or(|vt| !value_type_has_rank(vt));
        if unshaped {
            errors.push(ValidationError::MissingShape {
                kind: "input",
                name: vi.name.clone(),
            });
        }
    }

    ValidationReport { errors }
}

fn value_type_has_rank(vt: &super::types::ValueType) -> bool {
    match vt {
        super::types::ValueType::Tensor(t) | super::types::ValueType::SparseTensor(t) => {
            t.shape.is_some()
        }
        super::types::ValueType::Sequence(inner) | super::types::ValueType::Optional(inner) => {
            value_type_has_rank(inner)
        }
        super::types::ValueType::Map { value, .. } => value_type_has_rank(value),
        super::types::ValueType::Opaque { .. } => true,
    }
}

fn detect_cycle(graph: &Graph) -> Option<Vec<String>> {
    let n = graph.nodes.len();
    if n == 0 {
        return None;
    }
    let mut index_of: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
    for (i, node) in graph.nodes.iter().enumerate() {
        for out in &node.outputs {
            if !out.is_empty() {
                index_of.entry(out.as_str()).or_insert(i);
            }
        }
    }

    let mut deps: Vec<Vec<usize>> = vec![Vec::new(); n];
    for (i, node) in graph.nodes.iter().enumerate() {
        for inp in &node.inputs {
            if inp.is_empty() {
                continue;
            }
            if let Some(&j) = index_of.get(inp.as_str())
                && j != i
            {
                deps[i].push(j);
            }
        }
    }

    const WHITE: u8 = 0;
    const GRAY: u8 = 1;
    const BLACK: u8 = 2;
    let mut color = vec![WHITE; n];
    let mut stack: Vec<usize> = Vec::new();

    for start in 0..n {
        if color[start] != WHITE {
            continue;
        }
        stack.clear();
        stack.push(start);
        color[start] = GRAY;
        let mut iter_pos = vec![0usize; n];

        while let Some(&cur) = stack.last() {
            if iter_pos[cur] < deps[cur].len() {
                let next = deps[cur][iter_pos[cur]];
                iter_pos[cur] += 1;
                match color[next] {
                    GRAY => {
                        let pos = stack.iter().rposition(|&x| x == next).unwrap_or(0);
                        let mut path: Vec<String> = stack[pos..]
                            .iter()
                            .map(|&i| node_label(&graph.nodes[i]))
                            .collect();
                        path.push(node_label(&graph.nodes[next]));
                        return Some(path);
                    }
                    WHITE => {
                        color[next] = GRAY;
                        stack.push(next);
                    }
                    _ => {}
                }
            } else {
                color[cur] = BLACK;
                stack.pop();
            }
        }
    }
    None
}

fn node_label(node: &super::graph::Node) -> String {
    node.name
        .clone()
        .unwrap_or_else(|| format!("{}({})", node.op_type, node.outputs.join(",")))
}

pub fn validate_model(model: &Model) -> ValidationReport {
    validate_graph(&model.graph)
}

pub fn is_dim_fixed(d: &Dim) -> bool {
    matches!(d, Dim::Fixed(_))
}