diff --git a/process_mining/src/core/process_models/case_centric/petri_net/petri_net_struct.rs b/process_mining/src/core/process_models/case_centric/petri_net/petri_net_struct.rs index 5c461df..7c1ac32 100644 --- a/process_mining/src/core/process_models/case_centric/petri_net/petri_net_struct.rs +++ b/process_mining/src/core/process_models/case_centric/petri_net/petri_net_struct.rs @@ -11,6 +11,8 @@ use crate::core::process_models::case_centric::petri_net::pnml::{ export_pnml, import_pnml::{self, PNMLParseError}, }; +use crate::core::process_models::process_tree::ProcessTree; + #[derive( Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, PartialOrd, Ord, JsonSchema, )] @@ -448,6 +450,13 @@ impl PetriNet { } } +/// Creates a [`PetriNet`] from a [`ProcessTree`] +impl From for PetriNet { + fn from(process_tree: ProcessTree) -> Self { + process_tree.to_petri_net() + } +} + #[cfg(test)] mod tests { pub const SAMPLE_JSON_NET: &str = r#" diff --git a/process_mining/src/core/process_models/case_centric/process_tree/image_export.rs b/process_mining/src/core/process_models/case_centric/process_tree/image_export.rs new file mode 100644 index 0000000..ab2b9c6 --- /dev/null +++ b/process_mining/src/core/process_models/case_centric/process_tree/image_export.rs @@ -0,0 +1,225 @@ +//! Image Export of process trees +//! +//! 🔐 Requires the `graphviz-export` feature to be enabled +use std::{fs::File, io::Write}; + +use crate::core::process_models::process_tree::{ + process_tree_struct, Leaf, LeafLabel, Operator, ProcessTree, +}; +use graphviz_rust::{ + cmd::Format, + dot_generator::{attr, edge, graph, id, node, node_id, stmt}, + dot_structures::*, + printer::{DotPrinter, PrinterContext}, +}; +use macros_process_mining::register_binding; +use uuid::Uuid; + +/// +/// Export the image of a [`ProcessTree`] +/// +/// Also see [`export_process_tree_image_svg`] and [`export_process_tree_image_png`] +/// +pub fn export_process_tree_image>( + process_tree: &ProcessTree, + path: P, + format: Format, + dpi_factor: Option, +) -> Result<(), std::io::Error> { + let g = export_pt_to_dot_graph(process_tree, dpi_factor); + + g.print(&mut PrinterContext::default()); + + let out = graphviz_rust::exec(g, &mut PrinterContext::default(), vec![format.into()])?; + + let mut f = File::create(path)?; + f.write_all(&out)?; + Ok(()) +} + +/// +/// Creates the `Graphviz` code for a [`Node`] and expects a [`Uuid`] that is consistent +/// throughout the graph +/// +fn tree_node_to_gviz_node(node: &process_tree_struct::Node, node_id: Uuid) -> Stmt { + match node { + process_tree_struct::Node::Operator(op) => operator_to_node(op, node_id), + process_tree_struct::Node::Leaf(leaf) => leaf_to_node(leaf, node_id), + } +} + +/// +/// Creates the `Graphviz` code for a [`Operator`] and expects a [`Uuid`] that is consistent +/// throughout the graph +/// +fn operator_to_node(op: &Operator, op_id: Uuid) -> Stmt { + let symbol = op.operator_type.to_string(); + let shape = "circle"; + let size = 0.5; + stmt!( + node!(esc op_id; attr!("label", esc symbol), attr!("shape", shape), attr!("fixedsize", true), attr!("width", size), attr!("height", size)) + ) +} + +/// +/// Creates the `Graphviz` code for a [`Leaf`] and expects a [`Uuid`] that is consistent +/// throughout the graph +/// +fn leaf_to_node(leaf: &Leaf, leaf_id: Uuid) -> Stmt { + let (label, is_silent) = match &leaf.activity_label { + LeafLabel::Activity(act_label) => (act_label.clone(), false), + LeafLabel::Tau => ("".to_string(), true), + }; + let (font_size, width, height) = (12, 1, 0.5); + let fill_color = if is_silent { "black" } else { "white" }; + stmt!( + node!(esc leaf_id; attr!("label", esc label), attr!("shape", "box"), attr!("fontsize", font_size), attr!("style", "filled"), attr!("fillcolor", fill_color), attr!("width", width), attr!("height", height)) + ) +} + +/// +/// Creates the `Graphviz` code for an edge between two process tree [`Node`]s and expects two [`Uuid`] +/// that are consistent throughout the graph for creating the edge +/// +fn arc_to_edge(from: Uuid, to: Uuid) -> Stmt { + let attrs = Vec::default(); + + stmt!(edge!(node_id!(esc from) => node_id!(esc to), attrs)) +} + +/// +/// Export a [`ProcessTree`] to a DOT graph (used in Graphviz) +/// +pub fn export_pt_to_dot_graph(pt: &ProcessTree, dpi_factor: Option) -> Graph { + let mut gviz_nodes = Vec::new(); + let mut gviz_edges = Vec::new(); + + let root_id = Uuid::new_v4(); + gviz_nodes.push(tree_node_to_gviz_node(&pt.root, root_id)); + + let mut curr_nodes = vec![(root_id, &pt.root)]; + let mut next_nodes = Vec::new(); + + while !curr_nodes.is_empty() { + curr_nodes.iter().for_each(|(from_id, node)| match node { + process_tree_struct::Node::Operator(op) => op.children.iter().for_each(|child| { + let child_id = Uuid::new_v4(); + gviz_nodes.push(tree_node_to_gviz_node(child, child_id)); + gviz_edges.push(arc_to_edge(*from_id, child_id)); + next_nodes.push((child_id, child)); + }), + process_tree_struct::Node::Leaf(_) => {} + }); + + curr_nodes = next_nodes; + next_nodes = Vec::new(); + } + + let mut global_graph_options = vec![stmt!(GraphAttributes::Node(vec![ + attr!("fontname", esc "DejaVu Sans") + ]))]; + if let Some(dpi_fac) = dpi_factor { + global_graph_options.push(stmt!(attr!("dpi", (dpi_fac * 96.0)))) + } + + let g = graph!(strict di id!(esc Uuid::new_v4()),vec![global_graph_options, gviz_nodes, gviz_edges].into_iter().flatten().collect()); + g +} + +/// +/// Convert a DOT graph to a String containing the DOT source +/// +pub fn graph_to_dot(g: &Graph) -> String { + g.print(&mut PrinterContext::default()) +} + +/// +/// Export the image of a [`ProcessTree`] as a SVG file +/// +/// Also consider using [`ProcessTree::export_svg`] for convenience. +#[register_binding(stringify_error)] +pub fn export_process_tree_image_svg( + process_tree: &ProcessTree, + path: impl AsRef, +) -> Result<(), std::io::Error> { + export_process_tree_image(process_tree, path, Format::Svg, None) +} + +/// +/// Export the image of a [`ProcessTree`] as a PNG file +/// +/// Also consider using [`ProcessTree::export_png`] for convenience. +#[register_binding(stringify_error)] +pub fn export_process_tree_image_png( + process_tree: &ProcessTree, + path: impl AsRef, +) -> Result<(), std::io::Error> { + export_process_tree_image(process_tree, path, Format::Png, Some(2.0)) +} + +#[cfg(test)] +mod test { + use super::super::image_export::{ + export_process_tree_image_png, export_process_tree_image_svg, + }; + use crate::core::process_models::process_tree::{ + Leaf, Node, Operator, OperatorType, ProcessTree, + }; + use crate::test_utils::get_test_data_path; + + fn create_example_tree() -> ProcessTree { + let mut seq = Operator::new(OperatorType::Sequence); + let leaf_a = Leaf::new(Some("a".to_string())); + seq.children.push(Node::Leaf(leaf_a)); + + let mut conc = Operator::new(OperatorType::Concurrency); + let leaf_a = Leaf::new(Some("a".to_string())); + let leaf_b = Leaf::new(Some("b".to_string())); + + conc.children.push(Node::Leaf(leaf_a)); + conc.children.push(Node::Leaf(leaf_b)); + + let mut loop_op = Operator::new(OperatorType::Loop); + let leaf_e = Leaf::new(Some("e".to_string())); + let leaf_f = Leaf::new(Some("f".to_string())); + let leaf_silent = Leaf::new(None); + + loop_op.children.push(Node::Leaf(leaf_e)); + loop_op.children.push(Node::Operator(conc)); + loop_op.children.push(Node::Leaf(leaf_f)); + loop_op.children.push(Node::Leaf(leaf_silent)); + + let mut choice = Operator::new(OperatorType::ExclusiveChoice); + let leaf_b = Leaf::new(Some("b".to_string())); + let leaf_c = Leaf::new(Some("c".to_string())); + let leaf_d = Leaf::new(Some("d".to_string())); + choice.children.push(Node::Leaf(leaf_b)); + choice.children.push(Node::Leaf(leaf_c)); + choice.children.push(Node::Leaf(leaf_d)); + + seq.children.push(Node::Operator(loop_op)); + seq.children.push(Node::Operator(choice)); + + ProcessTree::new(Node::Operator(seq)) + } + + #[test] + pub fn test_petri_net_png_export() { + let tree = create_example_tree(); + + let export_path = get_test_data_path() + .join("export") + .join("process-tree-export-test.png"); + export_process_tree_image_png(&tree, export_path).unwrap(); + } + + #[test] + pub fn test_petri_net_svg_export() { + let tree = create_example_tree(); + + let export_path = get_test_data_path() + .join("export") + .join("process-tree-export-test.svg"); + export_process_tree_image_svg(&tree, export_path).unwrap(); + } +} diff --git a/process_mining/src/core/process_models/case_centric/process_tree/mod.rs b/process_mining/src/core/process_models/case_centric/process_tree/mod.rs index e674091..87fe92e 100644 --- a/process_mining/src/core/process_models/case_centric/process_tree/mod.rs +++ b/process_mining/src/core/process_models/case_centric/process_tree/mod.rs @@ -1,4 +1,7 @@ //! Process Tree +#[cfg(feature = "graphviz-export")] +pub mod image_export; pub(crate) mod process_tree_struct; + #[doc(inline)] pub use process_tree_struct::*; diff --git a/process_mining/src/core/process_models/case_centric/process_tree/process_tree_struct.rs b/process_mining/src/core/process_models/case_centric/process_tree/process_tree_struct.rs index 543f39e..85cec14 100644 --- a/process_mining/src/core/process_models/case_centric/process_tree/process_tree_struct.rs +++ b/process_mining/src/core/process_models/case_centric/process_tree/process_tree_struct.rs @@ -1,9 +1,13 @@ +use crate::core::process_models::petri_net::{ArcType, Marking, PlaceID}; +use crate::PetriNet; +use core::fmt; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// /// Leaf in a process tree /// -#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq, JsonSchema)] pub enum LeafLabel { /// Non-silent activity leaf Activity(String), @@ -14,7 +18,7 @@ pub enum LeafLabel { /// /// Node in a process tree /// -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, JsonSchema)] pub enum Node { /// Operator node of a process tree Operator(Operator), @@ -64,12 +68,32 @@ impl Node { Node::Leaf(_) => true, } } + + /// + /// Calls either [`Operator::add_to_petri_net`] or [`Leaf::add_to_petri_net`] depending on the + /// [`Node`] type. + /// Either takes given in and out places as the start and end of the inserted workflow net. + /// Edits the given Petri net by inserting the corresponding places and transitions of the + /// (sub)tree. + /// Returns the start and end places of the workflow net. + /// + pub fn add_to_petri_net( + &self, + net: &mut PetriNet, + in_place: Option, + out_place: Option, + ) -> (PlaceID, PlaceID) { + match self { + Node::Operator(op) => op.add_to_petri_net(net, in_place, out_place), + Node::Leaf(leaf) => leaf.add_to_petri_net(net, in_place, out_place), + } + } } /// /// Operator type enum for [`Operator`] /// -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, JsonSchema)] pub enum OperatorType { /// Sequence operator Sequence, @@ -81,10 +105,31 @@ pub enum OperatorType { Loop, } +/// +/// Functionality to translate the operators into ASCII: →, X, ∧, and ↻ +/// +impl fmt::Display for OperatorType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OperatorType::Sequence => { + write!(f, "→") + } + OperatorType::ExclusiveChoice => { + write!(f, "X") + } + OperatorType::Concurrency => { + write!(f, "∧") + } + OperatorType::Loop => { + write!(f, "↻") + } + } + } +} /// /// Object-centric process tree struct that contains [`Node`] as root /// -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ProcessTree { /// The root of the object-centric process tree pub root: Node, @@ -170,12 +215,57 @@ impl ProcessTree { result } + + /// + /// Transforms a [`ProcessTree`] into a [`PetriNet`] according to the rules defined in + /// "Process Mining: Data Science in Action" by Wil van der Aalst. + /// Returns a workflow net, consisting of the [`PetriNet`], its input, and output place's [`PlaceID`] + /// + pub fn to_petri_net(&self) -> PetriNet { + let mut petri_net = PetriNet::new(); + + let (start_place, end_place) = self.root.add_to_petri_net(&mut petri_net, None, None); + + petri_net.initial_marking = Some(Marking::from([(start_place, 1)])); + + let mut final_marking = Marking::new(); + final_marking.insert(end_place, 1); + petri_net.final_markings = Some(vec![final_marking]); + + petri_net + } + + #[cfg(feature = "graphviz-export")] + /// Export a process tree as a PNG image + /// + /// The PNG file is written to the specified filepath + /// + /// _Note_: This is an export method for __visualizing__ the process tree. + /// The resulting PNG file cannot be imported as a process tree again. + /// + /// Only available with the `graphviz-export` feature. + pub fn export_png>(&self, path: P) -> Result<(), std::io::Error> { + super::image_export::export_process_tree_image_png(self, path) + } + + #[cfg(feature = "graphviz-export")] + /// Export a process tree as an SVG image + /// + /// The SVG file is written to the specified filepath + /// + /// _Note_: This is an export method for __visualizing__ the process tree. + /// The resulting SVG file cannot be imported as a process tree again. + /// + /// Only available with the `graphviz-export` feature. + pub fn export_svg>(&self, path: P) -> Result<(), std::io::Error> { + super::image_export::export_process_tree_image_svg(self, path) + } } /// /// An operator node in a process tree /// -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Operator { /// The [`OperatorType`] of the tree itself pub operator_type: OperatorType, @@ -215,9 +305,125 @@ impl Operator { result } + + /// + /// Unfolds an operator and its descendants into corresponding place, transitions, and arcs and + /// adds them to the input [`PetriNet`]. This routine is executed recursively. + /// Optionally, the input and output place of the inserted workflow net can be defined. + /// + pub fn add_to_petri_net( + &self, + net: &mut PetriNet, + in_place: Option, + out_place: Option, + ) -> (PlaceID, PlaceID) { + let in_place = in_place.unwrap_or_else(|| net.add_place(None)); + let out_place = out_place.unwrap_or_else(|| net.add_place(None)); + + let num_of_children = self.children.len(); + + match self.operator_type { + // For a sequence operator, the workflow nets are sequentially connected using each + // previous output place as the input place of the following workflow net. + // The first and last children consider the operators input and output place as their + // input and output place, respectively. + OperatorType::Sequence => { + let mut last_in_place = in_place; + + self.children.iter().enumerate().for_each(|(pos, child)| { + let curr_out_place = { + if pos == num_of_children - 1 { + out_place + } else { + net.add_place(None) + } + }; + + child.add_to_petri_net(net, Some(last_in_place), Some(curr_out_place)); + + last_in_place = curr_out_place; + }) + } + // Considers for each child the input and output place of the operator as their input + // and output place + OperatorType::ExclusiveChoice => self.children.iter().for_each(|child| { + child.add_to_petri_net(net, Some(in_place), Some(out_place)); + }), + // Inserts and connects additional silent transitions as start and end and each child + // creates new input and output places that are then connected to the silent start and + // silent end transition. + OperatorType::Concurrency => { + let tau_start_transition = net.add_transition(None, None); + let tau_end_transition = net.add_transition(None, None); + + net.add_arc( + ArcType::place_to_transition(in_place, tau_start_transition), + None, + ); + net.add_arc( + ArcType::transition_to_place(tau_end_transition, out_place), + None, + ); + + self.children.iter().for_each(|child| { + let (child_start, child_end) = child.add_to_petri_net(net, None, None); + + net.add_arc( + ArcType::transition_to_place(tau_start_transition, child_start), + None, + ); + net.add_arc( + ArcType::place_to_transition(child_end, tau_end_transition), + None, + ); + }) + } + // Inserts silent transitions to put the workflow net of the loop operator in choice + // if other operators are in choice. All workflow nets share the same input and output + // places. However, only the first child models the do-part going from input to output + // place and every other child going from output to input place modelling the redo-part. + OperatorType::Loop => { + let tau_start_transition = net.add_transition(None, None); + let tau_end_transition = net.add_transition(None, None); + + net.add_arc( + ArcType::place_to_transition(in_place, tau_start_transition), + None, + ); + net.add_arc( + ArcType::transition_to_place(tau_end_transition, out_place), + None, + ); + + let loop_start_place = net.add_place(None); + let loop_end_place = net.add_place(None); + + net.add_arc( + ArcType::transition_to_place(tau_start_transition, loop_start_place), + None, + ); + net.add_arc( + ArcType::place_to_transition(loop_end_place, tau_end_transition), + None, + ); + + self.children.iter().enumerate().for_each(|(pos, child)| { + let (child_start, child_end) = if pos == 0 { + (loop_start_place, loop_end_place) + } else { + (loop_end_place, loop_start_place) + }; + + child.add_to_petri_net(net, Some(child_start), Some(child_end)); + }) + } + } + + (in_place, out_place) + } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, JsonSchema)] /// /// A leaf in a process tree /// @@ -242,6 +448,39 @@ impl Leaf { } } } + + /// + /// Adds a transition to represent the leaf of a tree. Optionally, input and output + /// places can be given to connect the newly created (silent) transition to. The output is + /// the [`PlaceID`] of the input and output place, each. + /// + pub fn add_to_petri_net( + &self, + net: &mut PetriNet, + in_place: Option, + out_place: Option, + ) -> (PlaceID, PlaceID) { + let in_place = in_place.unwrap_or_else(|| net.add_place(None)); + let out_place = out_place.unwrap_or_else(|| net.add_place(None)); + + let leaf_transition = { + match &self.activity_label { + LeafLabel::Activity(label) => net.add_transition(Some(label.clone()), None), + LeafLabel::Tau => net.add_transition(None, None), + } + }; + + net.add_arc( + ArcType::place_to_transition(in_place, leaf_transition), + None, + ); + net.add_arc( + ArcType::transition_to_place(leaf_transition, out_place), + None, + ); + + (in_place, out_place) + } } #[cfg(test)] @@ -328,4 +567,171 @@ mod tests { let pt = ProcessTree::new(Node::Operator(seq_node)); assert!(pt.is_valid()); } + + // Checking Seq(a,b,c) + #[test] + fn sequence_test() { + let mut seq = Operator::new(OperatorType::Sequence); + + let leaf_a = Leaf::new(Some("a".to_string())); + let leaf_b = Leaf::new(Some("b".to_string())); + let leaf_c = Leaf::new(Some("c".to_string())); + + seq.children.push(Node::Leaf(leaf_a)); + seq.children.push(Node::Leaf(leaf_b)); + seq.children.push(Node::Leaf(leaf_c)); + + let tree = ProcessTree::new(Node::Operator(seq)); + + let net = tree.to_petri_net(); + + assert_eq!(4, net.places.len()); + assert_eq!(3, net.transitions.len()); + assert_eq!(6, net.arcs.len()); + } + + // Checking Conc(a,b,c) + #[test] + fn concurrency_test() { + let mut conc = Operator::new(OperatorType::Concurrency); + + let leaf_a = Leaf::new(Some("a".to_string())); + let leaf_b = Leaf::new(Some("b".to_string())); + let leaf_c = Leaf::new(Some("c".to_string())); + + conc.children.push(Node::Leaf(leaf_a)); + conc.children.push(Node::Leaf(leaf_b)); + conc.children.push(Node::Leaf(leaf_c)); + + let tree = ProcessTree::new(Node::Operator(conc)); + + let net = tree.to_petri_net(); + + assert_eq!(8, net.places.len()); + assert_eq!(5, net.transitions.len()); + assert_eq!(14, net.arcs.len()); + } + + // Checking Loop(a,b,c) + #[test] + fn loop_test() { + let mut loop_op = Operator::new(OperatorType::Loop); + + let leaf_a = Leaf::new(Some("a".to_string())); + let leaf_b = Leaf::new(Some("b".to_string())); + let leaf_c = Leaf::new(Some("c".to_string())); + + loop_op.children.push(Node::Leaf(leaf_a)); + loop_op.children.push(Node::Leaf(leaf_b)); + loop_op.children.push(Node::Leaf(leaf_c)); + + let tree = ProcessTree::new(Node::Operator(loop_op)); + + let net = tree.to_petri_net(); + + assert_eq!(4, net.places.len()); + assert_eq!(5, net.transitions.len()); + assert_eq!(10, net.arcs.len()); + } + + // Checking Xor(a,b,c) + #[test] + fn choice_test() { + let mut choice = Operator::new(OperatorType::ExclusiveChoice); + + let leaf_a = Leaf::new(Some("a".to_string())); + let leaf_b = Leaf::new(Some("b".to_string())); + let leaf_c = Leaf::new(Some("c".to_string())); + + choice.children.push(Node::Leaf(leaf_a)); + choice.children.push(Node::Leaf(leaf_b)); + choice.children.push(Node::Leaf(leaf_c)); + + let tree = ProcessTree::new(Node::Operator(choice)); + + let net = tree.to_petri_net(); + + assert_eq!(2, net.places.len()); + assert_eq!(3, net.transitions.len()); + assert_eq!(6, net.arcs.len()); + } + + // Checking tau + #[test] + fn silent_test() { + let leaf_tau = Leaf::new(None); + + let tree = ProcessTree::new(Node::Leaf(leaf_tau)); + + let net = tree.to_petri_net(); + + assert_eq!(2, net.places.len()); + assert_eq!(1, net.transitions.len()); + assert_eq!(2, net.arcs.len()); + + net.transitions + .iter() + .for_each(|(_, t)| assert!(t.label.is_none())); + } + + // Checking a + #[test] + fn leaf_test() { + let leaf_a = Leaf::new(Some("a".to_string())); + + let tree = ProcessTree::new(Node::Leaf(leaf_a)); + + let net = tree.to_petri_net(); + + assert_eq!(2, net.places.len()); + assert_eq!(1, net.transitions.len()); + assert_eq!(2, net.arcs.len()); + + net.transitions + .iter() + .for_each(|(_, t)| assert_eq!("a", t.label.clone().unwrap())); + } + + // Checking Seq(a, Loop(e, Conc(a,b), f, tau), Xor(b, c, d)) + #[test] + fn all_op_test() { + let mut seq = Operator::new(OperatorType::Sequence); + let leaf_a = Leaf::new(Some("a".to_string())); + seq.children.push(Node::Leaf(leaf_a)); + + let mut conc = Operator::new(OperatorType::Concurrency); + let leaf_a = Leaf::new(Some("a".to_string())); + let leaf_b = Leaf::new(Some("b".to_string())); + + conc.children.push(Node::Leaf(leaf_a)); + conc.children.push(Node::Leaf(leaf_b)); + + let mut loop_op = Operator::new(OperatorType::Loop); + let leaf_e = Leaf::new(Some("e".to_string())); + let leaf_f = Leaf::new(Some("f".to_string())); + let leaf_silent = Leaf::new(None); + + loop_op.children.push(Node::Leaf(leaf_e)); + loop_op.children.push(Node::Operator(conc)); + loop_op.children.push(Node::Leaf(leaf_f)); + loop_op.children.push(Node::Leaf(leaf_silent)); + + let mut choice = Operator::new(OperatorType::ExclusiveChoice); + let leaf_b = Leaf::new(Some("b".to_string())); + let leaf_c = Leaf::new(Some("c".to_string())); + let leaf_d = Leaf::new(Some("d".to_string())); + choice.children.push(Node::Leaf(leaf_b)); + choice.children.push(Node::Leaf(leaf_c)); + choice.children.push(Node::Leaf(leaf_d)); + + seq.children.push(Node::Operator(loop_op)); + seq.children.push(Node::Operator(choice)); + let tree = ProcessTree::new(Node::Operator(seq)); + + let net = tree.to_petri_net(); + + assert_eq!(10, net.places.len()); + assert_eq!(13, net.transitions.len()); + assert_eq!(28, net.arcs.len()); + } }