arcweight 0.3.0

A high-performance, modular library for weighted finite state transducers with comprehensive examples and benchmarks
Documentation
//! FST visualization and drawing utilities.
//!
//! This module provides functions for visualizing FSTs in GraphViz DOT format,
//! enabling easy visualization of FST structure for debugging and documentation.
//!
//! # Overview
//!
//! The [`draw_fst`] function generates DOT format output that can be rendered
//! using GraphViz tools:
//!
//! ```bash
//! # Render to PNG
//! dot -Tpng fst.dot -o fst.png
//!
//! # Render to SVG (scalable)
//! dot -Tsvg fst.dot -o fst.svg
//!
//! # Render to PDF
//! dot -Tpdf fst.dot -o fst.pdf
//! ```
//!
//! # Examples
//!
//! ## Basic Usage
//!
//! ```
//! use arcweight::prelude::*;
//! use arcweight::utils::{draw_fst_default, DrawingConfig};
//!
//! let mut fst = VectorFst::<TropicalWeight>::new();
//! let s0 = fst.add_state();
//! let s1 = fst.add_state();
//! fst.set_start(s0);
//! fst.set_final(s1, TropicalWeight::one());
//! fst.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(0.5), s1));
//!
//! let dot = draw_fst_default(&fst).unwrap();
//! println!("{}", dot);
//! ```
//!
//! ## Custom Configuration
//!
//! ```
//! use arcweight::prelude::*;
//! use arcweight::utils::{draw_fst, DrawingConfig};
//!
//! let mut fst = VectorFst::<TropicalWeight>::new();
//! let s0 = fst.add_state();
//! fst.set_start(s0);
//! fst.set_final(s0, TropicalWeight::one());
//!
//! let config = DrawingConfig {
//!     show_weights: false,
//!     horizontal: false,
//!     node_shape: "box".to_string(),
//!     ..Default::default()
//! };
//!
//! let dot = draw_fst(&fst, config, None, None).unwrap();
//! ```
//!
//! # Complexity
//!
//! | Operation | Time | Space |
//! |-----------|------|-------|
//! | `draw_fst` | O(V + E) | O(V + E) for output string |
//!
//! # References
//!
//! - Emden Gansner, Eleftherios Koutsofios, and Stephen North. 2006.
//!   Drawing graphs with dot. <https://graphviz.org/documentation/>

use crate::fst::Fst;
use crate::semiring::Semiring;
use std::fmt::Write;

/// Configuration for FST drawing.
///
/// Controls the visual appearance and content of the generated DOT output.
/// Use [`Default::default()`] for sensible defaults.
///
/// # Examples
///
/// ```
/// use arcweight::utils::DrawingConfig;
///
/// // Default configuration
/// let config = DrawingConfig::default();
/// assert!(config.show_weights);
/// assert!(config.horizontal);
///
/// // Custom configuration
/// let config = DrawingConfig {
///     show_weights: false,
///     node_shape: "box".to_string(),
///     ..Default::default()
/// };
/// ```
#[derive(Debug, Clone)]
pub struct DrawingConfig {
    /// Whether to display arc weights on edges.
    pub show_weights: bool,
    /// Whether to display state ID numbers inside nodes.
    pub show_state_ids: bool,
    /// Whether to display final state weights.
    pub show_final_weights: bool,
    /// Whether to use symbol tables for human-readable labels.
    pub use_symbols: bool,
    /// Whether to use horizontal (left-to-right) layout. If false, uses top-to-bottom.
    pub horizontal: bool,
    /// GraphViz node shape (e.g., "circle", "box", "ellipse", "doublecircle").
    pub node_shape: String,
    /// Fill color for regular states.
    pub node_color: String,
    /// Fill color for the start state.
    pub start_color: String,
    /// Fill color for final states.
    pub final_color: String,
}

impl Default for DrawingConfig {
    fn default() -> Self {
        Self {
            show_weights: true,
            show_state_ids: true,
            show_final_weights: true,
            use_symbols: true,
            horizontal: true,
            node_shape: "circle".to_string(),
            node_color: "white".to_string(),
            start_color: "lightblue".to_string(),
            final_color: "lightgreen".to_string(),
        }
    }
}

/// Renders an FST to GraphViz DOT format.
///
/// Generates a DOT language representation of the FST that can be rendered
/// using GraphViz tools like `dot`, `neato`, or online viewers.
///
/// # Arguments
///
/// * `fst` - The FST to visualize
/// * `config` - Drawing configuration options
/// * `input_symbols` - Optional symbol table for input labels
/// * `output_symbols` - Optional symbol table for output labels
///
/// # Returns
///
/// A `Result` containing the DOT format string or a formatting error.
///
/// # Complexity
///
/// - **Time**: O(V + E) where V = states, E = arcs
/// - **Space**: O(V + E) for the output string
///
/// # Examples
///
/// ```
/// use arcweight::prelude::*;
/// use arcweight::utils::{draw_fst, DrawingConfig, SymbolTable};
///
/// let mut fst = VectorFst::<TropicalWeight>::new();
/// let s0 = fst.add_state();
/// let s1 = fst.add_state();
/// fst.set_start(s0);
/// fst.set_final(s1, TropicalWeight::new(0.5));
/// fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
///
/// // Without symbol tables
/// let dot = draw_fst(&fst, DrawingConfig::default(), None, None).unwrap();
/// assert!(dot.contains("digraph FST"));
///
/// // With symbol tables for readable labels
/// let mut syms = SymbolTable::new();
/// let a = syms.add_symbol("a");
/// let dot = draw_fst(&fst, DrawingConfig::default(), Some(&syms), Some(&syms)).unwrap();
/// ```
pub fn draw_fst<W, F>(
    fst: &F,
    config: DrawingConfig,
    input_symbols: Option<&crate::utils::SymbolTable>,
    output_symbols: Option<&crate::utils::SymbolTable>,
) -> Result<String, std::fmt::Error>
where
    W: Semiring + std::fmt::Display,
    F: Fst<W>,
{
    let mut output = String::new();

    // Write header
    writeln!(output, "digraph FST {{")?;
    if config.horizontal {
        writeln!(output, "  rankdir=LR;")?;
    }
    writeln!(output, "  size=\"8,5\";")?;
    writeln!(output, "  node [shape={}];", config.node_shape)?;

    // Draw states
    for state in fst.states() {
        let mut label = if config.show_state_ids {
            format!("{}", state)
        } else {
            String::new()
        };

        // Check if final
        let is_final = fst.is_final(state);
        let is_start = fst.start() == Some(state);

        if is_final && config.show_final_weights {
            if let Some(weight) = fst.final_weight(state) {
                if !label.is_empty() {
                    label.push_str("\\n");
                }
                write!(label, "{}", weight)?;
            }
        }

        // Set node style
        let mut style = format!("fillcolor={}", config.node_color);
        if is_start {
            style = format!("fillcolor={}", config.start_color);
        } else if is_final {
            style = format!("fillcolor={}", config.final_color);
        }

        writeln!(
            output,
            "  {} [label=\"{}\" style=filled {}];",
            state, label, style
        )?;
    }

    // Draw arcs
    for state in fst.states() {
        for arc in fst.arcs(state) {
            let ilabel_str = if config.use_symbols {
                if let Some(symbols) = input_symbols {
                    symbols
                        .find(arc.ilabel)
                        .unwrap_or(&format!("{}", arc.ilabel))
                        .to_string()
                } else {
                    format!("{}", arc.ilabel)
                }
            } else {
                format!("{}", arc.ilabel)
            };

            let olabel_str = if config.use_symbols {
                if let Some(symbols) = output_symbols {
                    symbols
                        .find(arc.olabel)
                        .unwrap_or(&format!("{}", arc.olabel))
                        .to_string()
                } else {
                    format!("{}", arc.olabel)
                }
            } else {
                format!("{}", arc.olabel)
            };

            let arc_label = if ilabel_str == olabel_str {
                if config.show_weights {
                    format!("{} / {}", ilabel_str, arc.weight)
                } else {
                    ilabel_str
                }
            } else if config.show_weights {
                format!("{}:{} / {}", ilabel_str, olabel_str, arc.weight)
            } else {
                format!("{}:{}", ilabel_str, olabel_str)
            };

            writeln!(
                output,
                "  {} -> {} [label=\"{}\"];",
                state, arc.nextstate, arc_label
            )?;
        }
    }

    writeln!(output, "}}")?;
    Ok(output)
}

/// Renders an FST to GraphViz DOT format with default configuration.
///
/// This is a convenience wrapper around [`draw_fst`] that uses default
/// configuration and no symbol tables.
///
/// # Examples
///
/// ```
/// use arcweight::prelude::*;
/// use arcweight::utils::draw_fst_default;
///
/// let mut fst = VectorFst::<TropicalWeight>::new();
/// let s0 = fst.add_state();
/// fst.set_start(s0);
/// fst.set_final(s0, TropicalWeight::one());
///
/// let dot = draw_fst_default(&fst).unwrap();
/// assert!(dot.contains("digraph FST"));
/// ```
pub fn draw_fst_default<W, F>(fst: &F) -> Result<String, std::fmt::Error>
where
    W: Semiring + std::fmt::Display,
    F: Fst<W>,
{
    draw_fst(fst, DrawingConfig::default(), None, None)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::prelude::*;

    #[test]
    fn test_drawing_config_default() {
        let config = DrawingConfig::default();
        assert!(config.show_weights);
        assert!(config.show_state_ids);
        assert!(config.horizontal);
        assert_eq!(config.node_shape, "circle");
    }

    #[test]
    fn test_draw_fst_default() {
        let mut fst = VectorFst::<TropicalWeight>::new();
        let s0 = fst.add_state();
        let s1 = fst.add_state();
        fst.set_start(s0);
        fst.set_final(s1, TropicalWeight::one());
        fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));

        let dot = draw_fst_default(&fst).unwrap();
        assert!(dot.contains("digraph FST"));
        assert!(dot.contains("rankdir=LR"));
    }

    #[test]
    fn test_draw_fst_with_config() {
        let mut fst = VectorFst::<TropicalWeight>::new();
        let s0 = fst.add_state();
        fst.set_start(s0);
        fst.set_final(s0, TropicalWeight::one());

        let config = DrawingConfig {
            horizontal: false,
            node_shape: "box".to_string(),
            ..Default::default()
        };

        let dot = draw_fst(&fst, config, None, None).unwrap();
        assert!(dot.contains("digraph FST"));
        assert!(!dot.contains("rankdir=LR"));
        assert!(dot.contains("shape=box"));
    }

    #[test]
    fn test_draw_fst_with_symbols() {
        let mut fst = VectorFst::<TropicalWeight>::new();
        let mut symbols = SymbolTable::new();
        let hello_id = symbols.add_symbol("hello");

        let s0 = fst.add_state();
        let s1 = fst.add_state();
        fst.set_start(s0);
        fst.set_final(s1, TropicalWeight::one());
        fst.add_arc(s0, Arc::new(hello_id, hello_id, TropicalWeight::one(), s1));

        let config = DrawingConfig {
            use_symbols: true,
            ..Default::default()
        };

        let dot = draw_fst(&fst, config, Some(&symbols), Some(&symbols)).unwrap();
        assert!(dot.contains("hello"));
    }
}