Dot file output

TODO put everything in a library
This commit is contained in:
elvis
2025-07-07 22:45:02 +02:00
parent cdf3d23243
commit 8baa1bafc1
5 changed files with 419 additions and 11 deletions

View File

@ -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<RSsystem, RSlabel, Directed, u32>;
pub fn digraph(
system: RSsystem
) -> Result<Graph<RSsystem, RSlabel>, String> {
let mut graph: Graph<RSsystem, RSlabel> = Graph::new();
) -> Result<RSgraph, String> {
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<String, String, Directed, u32>;
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<RSgraph>
) -> Box<RSformatNodeTy>
{
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<RSgraph>
) -> Box<RSformatEdgeTy>
{
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"
}
))
}

View File

@ -8,3 +8,4 @@ pub mod support_structures;
pub mod transitions;
pub mod translator;
pub mod graph;
pub mod rsdot;

341
src/rsprocess/rsdot.rs Normal file
View File

@ -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.
///
/// <https://graphviz.org/docs/attrs/rankdir/>
#[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<RankDir>,
NodeStyle: Option<NodeStyle>,
EdgeStyle: Option<EdgeStyle>,
}
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<G> RSDot<'_, G>
where
G: IntoNodeReferences + IntoEdgeReferences + NodeIndexable + GraphProp + DataMap,
{
fn graph_fmt<NF, EF>(
&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<G> 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<G> 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<G> 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<G> 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>(W);
impl<W> fmt::Write for Escaper<W>
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>(T);
impl<T> fmt::Display for Escaped<T>
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)
}
}

View File

@ -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 {