From 8baa1bafc1feddb124729521d1d4f38a7612b11a Mon Sep 17 00:00:00 2001 From: elvis Date: Mon, 7 Jul 2025 22:45:02 +0200 Subject: [PATCH] Dot file output TODO put everything in a library --- src/examples.rs | 21 ++- src/rsprocess/graph.rs | 63 ++++++- src/rsprocess/mod.rs | 1 + src/rsprocess/rsdot.rs | 341 +++++++++++++++++++++++++++++++++++++ src/rsprocess/structure.rs | 4 + 5 files changed, 419 insertions(+), 11 deletions(-) create mode 100644 src/rsprocess/rsdot.rs diff --git a/src/examples.rs b/src/examples.rs index 48c9595..7b46c0d 100644 --- a/src/examples.rs +++ b/src/examples.rs @@ -1,9 +1,7 @@ #![allow(dead_code)] -use petgraph::dot::Dot; - use crate::rsprocess::structure::{RSset, RSsystem}; -use crate::rsprocess::{graph, translator}; +use crate::rsprocess::{graph, rsdot, translator}; use crate::rsprocess::translator::Translator; use crate::rsprocess::{frequency, perpetual, statistics, transitions}; @@ -251,10 +249,11 @@ pub fn digraph() -> std::io::Result<()> { } }; - println!("Generated graph in dot notation:\n{:?}\n", Dot::new(&res)); - let rc_translator = Rc::new(translator); + let old_res = Rc::new(res.clone()); + + // map each value to the corresponding value we want to display let res = res.map( |id, node| graph::GraphMapNodesTy::from(graph::GraphMapNodes::Entities, @@ -263,9 +262,17 @@ pub fn digraph() -> std::io::Result<()> { &graph::GraphMapNodesTy::from(graph::GraphMapNodes::Context, Rc::clone(&rc_translator)).get()(id, node), graph::GraphMapEdgesTy::from(graph::GraphMapEdges::EntitiesAdded, - Rc::clone(&rc_translator)).get()); + Rc::clone(&rc_translator)).get() + ); - println!("Generated graph in dot notation:\n{}", Dot::new(&res)); + println!("Generated graph in dot notation:\n{}", + rsdot::RSDot::with_attr_getters( + &res, + &[], + &graph::default_edge_formatter(Rc::clone(&old_res)), + &graph::default_node_formatter(Rc::clone(&old_res)), + ) + ); Ok(()) } diff --git a/src/rsprocess/graph.rs b/src/rsprocess/graph.rs index d4a9f30..d49269d 100644 --- a/src/rsprocess/graph.rs +++ b/src/rsprocess/graph.rs @@ -1,16 +1,18 @@ #![allow(dead_code)] -use petgraph::Graph; +use petgraph::{Graph, Directed}; use std::collections::HashMap; -use super::structure::{RSlabel, RSsystem, RSset}; +use super::structure::{RSlabel, RSsystem, RSset, RSprocess}; use super::support_structures::TransitionsIterator; use super::translator; use std::rc::Rc; +type RSgraph = Graph; + pub fn digraph( system: RSsystem -) -> Result, String> { - let mut graph: Graph = Graph::new(); +) -> Result { + let mut graph = Graph::default(); let node = graph.add_node(system.clone()); let mut association = HashMap::new(); @@ -351,3 +353,56 @@ impl GraphMapEdgesTy { &self.function } } + +// ----------------------------------------------------------------------------- +// Formatting Nodes & Edges +// ----------------------------------------------------------------------------- +use petgraph::visit::{IntoNodeReferences, IntoEdgeReferences, EdgeRef}; + +type RSdotGraph = Graph; +type RSformatNodeTy = + dyn Fn(&RSdotGraph, <&RSdotGraph as IntoNodeReferences>::NodeRef) -> String; + +type RSformatEdgeTy = + dyn Fn(&RSdotGraph, <&RSdotGraph as IntoEdgeReferences>::EdgeRef) -> String; + +pub fn default_node_formatter( + original_graph: Rc +) -> Box +{ + Box::new( + move |_g, n| + String::from( + match original_graph.node_weight(n.0).unwrap().get_context_process() + { + RSprocess::Nill => + ", fillcolor=white", + RSprocess::RecursiveIdentifier { identifier: _ } => + ", fillcolor=\"#BBFF99\"", + RSprocess::EntitySet { entities: _, next_process: _ } => + ", fillcolor=\"#AAEEFF\"", + RSprocess::NondeterministicChoice { children: _ } => + ", fillcolor=\"#FFEE99\"", + RSprocess::Summation { children: _ } => + ", fillcolor=\"#CC99FF\"", + RSprocess::WaitEntity + { repeat: _, repeated_process: _, next_process: _ } => + ", fillcolor=\"#FF99AA\"", + } + ) + ) +} + +pub fn default_edge_formatter( + original_graph: Rc +) -> Box +{ + Box::new( + move |_g, e| String::from( + if original_graph.edge_weight(e.id()).unwrap().products.is_empty() { + "color=black, fontcolor=black" + } else { + "color=blue, fontcolor=blue" + } + )) +} diff --git a/src/rsprocess/mod.rs b/src/rsprocess/mod.rs index 52662b0..0249f56 100644 --- a/src/rsprocess/mod.rs +++ b/src/rsprocess/mod.rs @@ -8,3 +8,4 @@ pub mod support_structures; pub mod transitions; pub mod translator; pub mod graph; +pub mod rsdot; diff --git a/src/rsprocess/rsdot.rs b/src/rsprocess/rsdot.rs new file mode 100644 index 0000000..0f9ee77 --- /dev/null +++ b/src/rsprocess/rsdot.rs @@ -0,0 +1,341 @@ +//! Slightly modified Simple graphviz dot file format output. +//! See petgraph::dot::mod. +#![allow(dead_code)] + +// use alloc::string::String; +use core::fmt::{self, Display, Write}; +use petgraph:: +{ + data::DataMap, + visit::{ + EdgeRef, + GraphProp, + IntoEdgeReferences, + IntoNodeReferences, + NodeIndexable, + NodeRef, +}}; + +pub struct RSDot<'a, G> +where + G: IntoEdgeReferences + IntoNodeReferences + DataMap, +{ + graph: G, + get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String, + get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String, + config: Configs, +} + +static TYPE: [&str; 2] = ["graph", "digraph"]; +static EDGE: [&str; 2] = ["--", "->"]; +static INDENT: &str = " "; + +impl<'a, G> RSDot<'a, G> +where + G: IntoNodeReferences + IntoEdgeReferences + DataMap, +{ + /// Create a `Dot` formatting wrapper with default configuration. + #[inline] + pub fn new(graph: G) -> Self { + RSDot { + graph, + get_edge_attributes: &|_, _| String::new(), + get_node_attributes: &|_, _| String::new(), + config: Configs::default(), + } + } + + /// Create a `Dot` formatting wrapper with custom configuration. + #[inline] + pub fn with_config(graph: G, config: &'a [Config]) -> Self { + let config = Configs::extract(config); + RSDot { + graph, + get_edge_attributes: &|_, _| String::new(), + get_node_attributes: &|_, _| String::new(), + config, + } + } + + #[inline] + pub fn with_attr_getters( + graph: G, + config: &'a [Config], + get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String, + get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String, + ) -> Self { + let config = Configs::extract(config); + RSDot { + graph, + get_edge_attributes, + get_node_attributes, + config, + } + } +} + +/// Direction of graph layout. +/// +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum RankDir { + /// Top to bottom + #[default] + TB, + /// Bottom to top + BT, + /// Left to right + LR, + /// Right to left + RL, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NodeStyle { + shape: String, + style: String +} + +impl Default for NodeStyle { + fn default() -> Self { + NodeStyle { shape: "box".to_string(), + style: "filled, rounded".to_string() } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EdgeStyle { + arrowhead: String +} + +impl Default for EdgeStyle { + fn default() -> Self { + EdgeStyle { arrowhead: "vee".to_string() } + } +} + +/// `Dot` configuration. +/// +/// This enum does not have an exhaustive definition (will be expanded) +#[derive(Debug, PartialEq, Eq)] +pub enum Config { + /// Sets direction of graph layout. + RankDir(RankDir), + /// Node style + NodeStyle(NodeStyle), + /// Edge style + EdgeStyle(EdgeStyle), +} +macro_rules! make_config_struct { + ($($variant:ident,)*) => { + #[allow(non_snake_case)] + struct Configs { + $($variant: bool,)* + RankDir: Option, + NodeStyle: Option, + EdgeStyle: Option, + } + impl Configs { + #[inline] + fn extract(configs: &[Config]) -> Self { + let mut conf = Self::default(); + for c in configs { + match c { + $(Config::$variant => conf.$variant = true,)* + Config::RankDir(dir) => conf.RankDir = Some(*dir), + Config::NodeStyle(style) => + conf.NodeStyle = Some(style.clone()), + Config::EdgeStyle(style) => + conf.EdgeStyle = Some(style.clone()), + } + } + conf + } + } + impl Default for Configs { + fn default() -> Self { + Configs { + $(Config::$variant: true,)* + RankDir: Some(RankDir::default()), + NodeStyle: Some(NodeStyle::default()), + EdgeStyle: Some(EdgeStyle::default()), + } + } + } + } +} + +make_config_struct!(); + +impl RSDot<'_, G> +where + G: IntoNodeReferences + IntoEdgeReferences + NodeIndexable + GraphProp + DataMap, +{ + fn graph_fmt( + &self, + f: &mut fmt::Formatter, + node_fmt: NF, + edge_fmt: EF + ) -> fmt::Result + where + NF: Fn(&G::NodeWeight, &mut fmt::Formatter) -> fmt::Result, + EF: Fn(&G::EdgeWeight, &mut fmt::Formatter) -> fmt::Result, + { + let g = self.graph; + writeln!(f, "{} {{", TYPE[g.is_directed() as usize])?; + + if let Some(rank_dir) = &self.config.RankDir { + let value = match rank_dir { + RankDir::TB => "TB", + RankDir::BT => "BT", + RankDir::LR => "LR", + RankDir::RL => "RL", + }; + writeln!(f, "{INDENT}rankdir=\"{value}\"\n")?; + } + + if let Some(style) = &self.config.NodeStyle { + writeln!(f, + "{INDENT}node [shape=\"{}\", style=\"{}\"]", + style.shape, + style.style)?; + } + + if let Some(style) = &self.config.EdgeStyle { + writeln!(f, "{INDENT}edge [arrowhead=\"{}\"]\n", style.arrowhead)?; + } + + // output all labels + for node in g.node_references() { + write!(f, "{INDENT}\"")?; + // Escaped(FnFmt(node.weight(), &node_fmt)).fmt(f)?; + write!(f, "{}", g.to_index(node.id()))?; + write!(f, "\" [ ")?; + + write!(f, "label = \"")?; + Escaped(FnFmt(node.weight(), &node_fmt)).fmt(f)?; + write!(f, "\" ")?; + + writeln!(f, "{}]", (self.get_node_attributes)(g, node))?; + } + // output all edges + for edge in g.edge_references() { + write!(f, "{INDENT}\"")?; + // let node_source_weight = g.node_weight(edge.source()).unwrap(); + // Escaped(FnFmt(node_source_weight, &node_fmt)).fmt(f)?; + write!(f, "{}", g.to_index(edge.source()))?; + write!(f, "\" {} \"", EDGE[g.is_directed() as usize])?; + // let node_target_weight = g.node_weight(edge.target()).unwrap(); + // Escaped(FnFmt(node_target_weight, &node_fmt)).fmt(f)?; + write!(f, "{}", g.to_index(edge.target()))?; + write!(f, "\" [ ")?; + + write!(f, "label = \"")?; + Escaped(FnFmt(edge.weight(), &edge_fmt)).fmt(f)?; + write!(f, "\" ")?; + + writeln!(f, "{}]", (self.get_edge_attributes)(g, edge))?; + } + + writeln!(f, "}}")?; + + Ok(()) + } +} + +impl fmt::Display for RSDot<'_, G> +where + G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp + DataMap, + G::EdgeWeight: fmt::Display, + G::NodeWeight: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.graph_fmt(f, fmt::Display::fmt, fmt::Display::fmt) + } +} + +impl fmt::LowerHex for RSDot<'_, G> +where + G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp + DataMap, + G::EdgeWeight: fmt::LowerHex, + G::NodeWeight: fmt::LowerHex, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.graph_fmt(f, fmt::LowerHex::fmt, fmt::LowerHex::fmt) + } +} + +impl fmt::UpperHex for RSDot<'_, G> +where + G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp + DataMap, + G::EdgeWeight: fmt::UpperHex, + G::NodeWeight: fmt::UpperHex, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.graph_fmt(f, fmt::UpperHex::fmt, fmt::UpperHex::fmt) + } +} + +impl fmt::Debug for RSDot<'_, G> +where + G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp + DataMap, + G::EdgeWeight: fmt::Debug, + G::NodeWeight: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.graph_fmt(f, fmt::Debug::fmt, fmt::Debug::fmt) + } +} + +/// Escape for Graphviz +struct Escaper(W); + +impl fmt::Write for Escaper +where + W: fmt::Write, +{ + fn write_str(&mut self, s: &str) -> fmt::Result { + for c in s.chars() { + self.write_char(c)?; + } + Ok(()) + } + + fn write_char(&mut self, c: char) -> fmt::Result { + match c { + '"' | '\\' => self.0.write_char('\\')?, + // \l is for left justified linebreak + '\n' => return self.0.write_str("\\l"), + _ => {} + } + self.0.write_char(c) + } +} + +/// Pass Display formatting through a simple escaping filter +struct Escaped(T); + +impl fmt::Display for Escaped +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if f.alternate() { + writeln!(&mut Escaper(f), "{:#}", &self.0) + } else { + write!(&mut Escaper(f), "{}", &self.0) + } + } +} + +/// Format data using a specific format function +struct FnFmt<'a, T, F>(&'a T, F); + +impl<'a, T, F> fmt::Display for FnFmt<'a, T, F> +where + F: Fn(&'a T, &mut fmt::Formatter<'_>) -> fmt::Result, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.1(self.0, f) + } +} diff --git a/src/rsprocess/structure.rs b/src/rsprocess/structure.rs index 88b0817..19566c4 100644 --- a/src/rsprocess/structure.rs +++ b/src/rsprocess/structure.rs @@ -108,6 +108,10 @@ impl RSset { pub fn push(&mut self, b: &RSset) { self.identifiers.extend(b.iter()) } + + pub fn is_empty(&self) -> bool { + self.identifiers.is_empty() + } } impl Default for RSset {