Skip to main content
Rust4PM
RustYour own files

Examples from the rust4pm crate

The example programs that ship with the process_mining crate: log and OCEL statistics, Alpha+++ discovery, OCEL CSV export and OC-DECLARE, as published on crates.io.

The process_mining crate ships its own example programs in its examples/ folder. This page shows them for the version these docs describe, taken from the crate as published on crates.io when the site is built, so the code below is exactly what that release contains.

They are small command-line programs that take file paths as arguments. The repository's main branch can be ahead of the release, so to run one exactly as shown, put it into a project pinned to this version:

shell
cargo new r4pm-examples && cd r4pm-examples
cargo add process_mining@=0.6.2
mkdir examples   # save an example below as examples/event_log_stats.rs
cargo run --release --example event_log_stats -- road-traffic-fines.xes.gz

Unlike the snippets in the guides, they read their input from the command line, so they work with any file. CI runs the ones marked below against the sample data.

event_log_stats.rs

Imports an XES event log and prints basic statistics (trace count, event count, etc.).

CI runs it with: data/road-traffic-fines.xes.gz

cargo run --example event_log_stats -- <path_to_log.xes>
rust
use process_mining::{EventLog, Importable};
use std::env;
use std::error::Error;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: {} <path_to_event_log>", args[0]);
        std::process::exit(1);
    }

    let path = PathBuf::from(&args[1]);
    println!("Importing event log from {:?}", path);

    let log = EventLog::import_from_path(&path)?;
    println!("Successfully imported event log.");
    println!("Number of traces: {}", log.traces.len());

    let total_events: usize = log.traces.iter().map(|t| t.events.len()).sum();
    println!("Total number of events: {}", total_events);

    if !log.traces.is_empty() {
        let avg_events = total_events as f64 / log.traces.len() as f64;
        println!("Average events per trace: {:.2}", avg_events);
    }

    // Example: Print first trace ID if available
    if let Some(first_trace) = log.traces.first() {
        if let Some(attr) = first_trace
            .attributes
            .iter()
            .find(|a| a.key == "concept:name")
        {
            println!("First trace ID: {:?}", attr.value);
        }
    }
    Ok(())
}

process_discovery.rs

Imports an XES event log, discovers a Petri net using the Alpha+++ algorithm, and exports it to PNML.

CI runs it with: data/road-traffic-fines.xes.gz model.pnml

cargo run --example process_discovery -- <path_to_log.xes> <output_model.pnml>
rust
use process_mining::core::event_data::case_centric::utils::activity_projection::EventLogActivityProjection;
use process_mining::discovery::case_centric::alphappp::full::{
    alphappp_discover_petri_net, AlphaPPPConfig,
};
use process_mining::{EventLog, Exportable, Importable};
use std::env;
use std::error::Error;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = env::args().collect();
    if args.len() != 3 {
        eprintln!("Usage: {} <path_to_event_log> <output_pnml_path>", args[0]);
        std::process::exit(1);
    }

    let input_path = PathBuf::from(&args[1]);
    let output_path = PathBuf::from(&args[2]);

    println!("Importing event log from {:?}", input_path);
    let log = EventLog::import_from_path(&input_path)?;

    println!("Converting to activity projection...");
    let projection = EventLogActivityProjection::from(&log);

    println!("Discovering Petri net using Alpha+++...");
    let config = AlphaPPPConfig::default();
    let petri_net = alphappp_discover_petri_net(&projection, config);

    println!(
        "Discovered Petri net with {} places and {} transitions.",
        petri_net.places.len(),
        petri_net.transitions.len()
    );

    println!("Exporting Petri net to {:?}", output_path);
    petri_net.export_to_path(&output_path)?;

    println!("Done!");
    Ok(())
}

ocel_stats.rs

Imports an OCEL and prints basic statistics.

CI runs it with: data/order-management.xml.gz

cargo run --example ocel_stats -- <path_to_ocel.xml>
rust
use process_mining::core::event_data::object_centric::linked_ocel::{
    LinkedOCELAccess, SlimLinkedOCEL,
};
use process_mining::Importable;
use std::env;
use std::error::Error;
use std::path::PathBuf;
use std::time::Instant;

fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: {} <path_to_ocel_file>", args[0]);
        std::process::exit(1);
    }

    let path = PathBuf::from(&args[1]);
    println!("Importing OCEL from {:?}", path);
    let now = Instant::now();
    let ocel = SlimLinkedOCEL::import_from_path(&path)?;
    println!("Successfully imported OCEL in {:?}.", now.elapsed());
    println!("Number of events: {}", ocel.get_all_evs().count());
    println!("Number of objects: {}", ocel.get_all_obs().count());

    println!("Event Types: {:?}", ocel.get_ev_types().collect::<Vec<_>>());
    println!(
        "Object Types: {:?}",
        ocel.get_ob_types().collect::<Vec<_>>()
    );

    let preview_n = 10;
    println!("First {} events:", preview_n);
    for ev in ocel.get_all_evs().take(preview_n) {
        let ev_type = ocel.get_ev_type_of(ev);
        let timestamp = ocel.get_ev_time(ev);
        println!(
            "Event {:?}: Type: {}, Timestamp: {}",
            ev, ev_type, timestamp
        );
        let attrs = ocel.get_ev_attrs(ev);
        for attr in attrs {
            let val = ocel.get_ev_attr_val(ev, attr);
            println!("  Attribute: {} = {:?}", attr, val);
        }
    }
    Ok(())
}

ocel_csv_export.rs

Imports an OCEL and exports it to CSV format.

CI runs it with: data/order-management.xml.gz out.ocel.csv

cargo run --example ocel_csv_export -- <path_to_ocel.xml> [output.ocel.csv]
rust
//! Example: Export OCEL to CSV format
//!
//! This example demonstrates importing an OCEL file (XML/JSON) and exporting it to
//! the CSV-based OCEL format.
//!
//! # CSV Format Behavior
//!
//! The CSV export follows these rules:
//!
//! ## Object Attribute Changes
//!
//! - **At event time**: If an object has attribute changes at the exact same timestamp as an
//!   event that involves that object, the attributes are included inline in the object reference
//!   (e.g., `i1#qualifier{"attr": "value"}`).
//!
//! - **Standalone changes**: If object attributes change at a time when no event involves that
//!   object, a separate row is created with empty `id` and `activity` columns.
//!
//! - **Multiple attributes at same time**: All attribute changes for an object at the same
//!   timestamp are combined into a single JSON object.
//!
//! - **Unchanged values**: The format does not track whether values actually changed - it records
//!   all attribute values at their specified timestamps. If the OCEL source has duplicate entries
//!   with the same value, they will all be exported.
//!
//! ## Object-to-Object (O2O) Relationships
//!
//! - O2O relationships are exported as rows with `o2o` in the activity column.
//! - The source object ID is in the `id` column.
//! - Target objects are in the appropriate `ot:` columns with their qualifiers.
//!
//! - Event attribute types are inferred as strings during CSV import
//! - Column ordering may differ
//!
//! # Usage
//!
//! ```bash
//! cargo run --example ocel_csv_export -- <input_ocel.xml|json> <output.ocel.csv>
//! ```

use process_mining::core::event_data::object_centric::ocel_csv::export_ocel_csv_to_path;
use process_mining::{Importable, OCEL};
use std::env;
use std::error::Error;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 || args.len() > 3 {
        eprintln!("Usage: {} <input_ocel_file> [output.ocel.csv]", args[0]);
        eprintln!();
        eprintln!("Imports an OCEL file (XML/JSON) and exports it to CSV format.");
        eprintln!();
        eprintln!("Arguments:");
        eprintln!("  input_ocel_file   Path to input OCEL file (.xml, .json, .jsonocel)");
        eprintln!("  output.ocel.csv   Optional output path (default: input name + .ocel.csv)");
        std::process::exit(1);
    }

    let input_path = PathBuf::from(&args[1]);
    let output_path = if args.len() == 3 {
        PathBuf::from(&args[2])
    } else {
        // Default output: same name with .ocel.csv extension
        let stem = input_path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("output");
        input_path.with_file_name(format!("{}.ocel.csv", stem))
    };

    println!("Importing OCEL from {:?}", input_path);
    let ocel = OCEL::import_from_path(&input_path)?;

    println!("Successfully imported OCEL:");
    println!("  Events: {}", ocel.events.len());
    println!("  Objects: {}", ocel.objects.len());
    println!(
        "  Event Types: {}",
        ocel.event_types
            .iter()
            .map(|et| et.name.as_str())
            .collect::<Vec<_>>()
            .join(", ")
    );
    println!(
        "  Object Types: {}",
        ocel.object_types
            .iter()
            .map(|ot| ot.name.as_str())
            .collect::<Vec<_>>()
            .join(", ")
    );

    // Count object attributes and O2O relationships
    let total_obj_attrs: usize = ocel.objects.iter().map(|o| o.attributes.len()).sum();
    let total_o2o_rels: usize = ocel.objects.iter().map(|o| o.relationships.len()).sum();
    println!("  Object Attributes: {}", total_obj_attrs);
    println!("  O2O Relationships: {}", total_o2o_rels);

    println!();
    println!("Exporting to CSV: {:?}", output_path);

    // Use default options (includes O2O and object attribute changes)
    export_ocel_csv_to_path(&ocel, &output_path)?;

    println!("Successfully exported OCEL to CSV format.");

    // Print a preview of the file
    println!();
    println!("Preview of output (first 10 lines):");
    println!("---");
    let content = std::fs::read_to_string(&output_path)?;
    for (i, line) in content.lines().take(10).enumerate() {
        // Truncate long lines for display
        let display_line = if line.len() > 120 {
            format!("{}...", &line[..117])
        } else {
            line.to_string()
        };
        println!("{}: {}", i + 1, display_line);
    }
    if content.lines().count() > 10 {
        println!("... ({} more lines)", content.lines().count() - 10);
    }
    println!("---");

    Ok(())
}

oc_declare.rs

Imports an OCEL, discovers OC-DECLARE constraints with the default options and prints how many it found.

CI runs it with: data/order-management.xml.gz

cargo run --example oc_declare -- <path_to_ocel.xml>
rust
use std::{env::args, path::PathBuf};

use process_mining::{
    core::event_data::object_centric::linked_ocel::SlimLinkedOCEL,
    discovery::object_centric::oc_declare::{
        discover_behavior_constraints, OCDeclareDiscoveryOptions,
    },
    Importable, OCEL,
};

pub fn main() {
    let path_opt = args().nth(1);
    if let Some(path) = path_opt.map(PathBuf::from) {
        let ocel = OCEL::import_from_path(&path).expect("Failed to import OCEL.");
        let locel = SlimLinkedOCEL::from_ocel(ocel);
        let discovered_constraints = discover_behavior_constraints(
            &locel,
            OCDeclareDiscoveryOptions {
                ..Default::default()
            },
        );
        println!(
            "Discovered {} OC-DECLARE constraints",
            discovered_constraints.len()
        );
    }
}

petri_net_import_export.rs

Imports a Petri net from PNML, prints stats, and exports it again.

Not run in CI: needs a PNML file as input, e.g., the output of process_discovery.

cargo run --example petri_net_import_export -- <input_model.pnml> <output_model.pnml>
rust
use process_mining::{Exportable, Importable, PetriNet};
use std::env;
use std::error::Error;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = env::args().collect();
    if args.len() != 3 {
        eprintln!("Usage: {} <input_pnml_path> <output_pnml_path>", args[0]);
        std::process::exit(1);
    }

    let input_path = PathBuf::from(&args[1]);
    let output_path = PathBuf::from(&args[2]);

    println!("Importing Petri net from {:?}", input_path);
    let petri_net = PetriNet::import_from_path(&input_path)?;

    println!("Petri net stats:");
    println!("  Places: {}", petri_net.places.len());
    println!("  Transitions: {}", petri_net.transitions.len());
    println!("  Arcs: {}", petri_net.arcs.len());

    println!("Exporting Petri net to {:?}", output_path);
    petri_net.export_to_path(&output_path)?;

    println!("Done!");
    Ok(())
}

ocel_duckdb_export.rs

Imports an OCEL and exports it to a DuckDB database.

Not run in CI: needs the ocel-duckdb feature.

cargo run --example ocel_duckdb_export -- <path_to_ocel.xml>
rust
use std::{collections::HashSet, env::args, path::PathBuf};

use process_mining::{Exportable, Importable, OCEL};
use std::error::Error;

pub fn main() -> Result<(), Box<dyn Error>> {
    let path_opt = args().nth(1);
    if let Some(mut path) = path_opt.map(PathBuf::from) {
        let mut ocel = OCEL::import_from_path(&path)?;
        // Including invalid E2O relations (i.e., to objects that do not exist) can cause corrupted or incomplete SQL exports
        // Thus, we filter the E2O relations to only keep valid ones
        let all_obj_ids: HashSet<_> = ocel.objects.iter().map(|o| &o.id).collect();
        for e in &mut ocel.events {
            e.relationships
                .retain(|r| all_obj_ids.contains(&r.object_id));
        }
        // Export
        path.set_file_name(format!(
            "{}.duckdb",
            path.file_name()
                .and_then(|p| p.to_str())
                .unwrap_or_default()
        ));
        ocel.export_to_path(&path)?;
    }
    Ok(())
}