Skip to main content

PM4Py-UCM — Process Mining with Use Case Maps

pm4py-ucm

tests docs Python License: GPL v3+ Streamlit V5 Streamlit V2

Use Case Map (UCM) extension for PM4Py.

pm4py-ucm adds first-class support for the Use Case Map modelling notation — part of the ITU-T Z.151 User Requirements Notation (URN) standard, supported by the open-source jUCMNav tool — to PM4Py-style process-mining workflows.

The package is structured as a drop-in companion to PM4Py's existing BPMN support: every public helper has the same shape (read_*, write_*, discover_*_inductive, view_*, convert_to_*) so adopting UCM as an additional output of process mining is a one-word change in user code.

import pm4py
import pm4py_ucm

log = pm4py.read_xes("running-example.xes")
ucm = pm4py_ucm.discover_ucm_inductive(log)
pm4py_ucm.view_ucm(ucm)
pm4py_ucm.write_ucm(ucm, "running-example.jucm")  # opens in jUCMNav

Ways to get started:

  • demo/pm4py_ucm_tutorial.ipynb — end-to-end Jupyter walkthrough on a real claims-payment log (discovery, BPMN/UCM rendering, navigable SVG + .jucm/.png/.svg export, performer mining, hierarchical decomposition, .jucm round-trips).
  • demo/scenario_synthesis_tutorial.ipynb — the scenario-synthesis layer: concurrency-aware variants, per-loop counters + LoopEntryGuard, both OR-fork condition encodings (variant-driven and data-driven / decision-mining, with the per-fork accuracy report), decomposed UCMs, and a capstone running both encodings on the claims log.
  • demo/model_families_tutorial.ipynb — the model-family pipeline on the claims log: attribute detection, partition preview, per-cell mining, stack/matrix rendering, combined export, the dynamic-stub umbrella (skeleton, resource variation, path scenarios), performance overlays on activities and edges (with the value-scaled heat-map), pairwise comparison, and the family statistics report (FamilyStats + the self-contained interactive HTML file).
  • demo/dashboards_tutorial.ipynb — the dashboards layer: the per-case fact table, the metric catalog and computing widgets, filters / targets / scorecard, every visualisation (KPI, gauge, histogram, box plot, bar, line, pie/donut, heatmap table), the ƒ custom-formula language, and exporting a self-contained interactive HTML dashboard.
  • demo/sessions_tutorial.ipynbsave, share & resume a project: the config-vs-data-vs-derived principle, the parameter registry and collect, a settings file vs a self-contained project bundle, the versioned dashboards envelope, load round-trips, and forward-compatibility (unknown keys preserved).
  • web/streamlit_app_v5.py — the V5 web app (model, scenarios, model families, statistics reports, Dashboards, plus a global log filter and activity renaming), hosted at https://pm4py-ucm.streamlit.app/ — click, don't code: upload an XES/CSV, tune the miner, download the result. (The V2 scenarios app stays at https://pm4py-ucm-scenarios.streamlit.app/.)
  • API reference — the full browsable docstring documentation (public API + every module), generated by pdoc and published to GitHub Pages on each push to main.
  • The rest of this README — reference docs for the public API.

Web front-end

The Streamlit front-end — now the V5 app (web/streamlit_app_v5.py) — serves the deployment: a left-rail workspace over these views (a strict superset of the earlier four-tab V3 app), with a global log filter and activity renaming applied before mining across every view:

  • Model — upload an event log (XES or CSV), tune the inductive miner / decomposition / performer settings interactively, optionally filter the log (range sliders over activity- and variant-frequency ranks, an exclude list, a date range, a cycle-time percentile band that keeps the fastest or slowest cases by end-to-end duration, and an attribute filter written in the ƒ metric language — e.g. attr("Channel") == "Web" and duration() > 5) and rename or merge activities before mining, preview the diagram in UCM or BPMN notation, and download the rendered PNG, the .jucm file, or the filtered log itself as XES/CSV. The filter and rename are global — every view below (and its exports) mines the same transformed log.

    Model view

  • Scenarios — concurrency-aware variant clustering + one executable jUCMNav ScenarioDef per variant. Both variant-driven and data-driven OR-fork encodings are exposed; the tab surfaces headline metrics (variant count, sequence variants, compression ratio, fitness %, per-fork condition-mining accuracies) and, behind a single Prepare downloads button, offers four downloads: the .jucm with the synthesized scenario group, variants.csv, case_variant_map.csv, and (data-driven mode) condition_mining.csv. Runs on flat and decomposed UCMs alike.

    Scenarios view

  • Family — a deterministic advisor ranks the case attributes by discriminative power (which one yields genuinely different processes), then pick 1–2 (with per-value filters and a pre-mining coverage heatmap) and mine one model per combination. A single Prepare downloads button then builds the per-cell zip, the combined .jucm, the dynamic-stub umbrella .jucm, the grid PNG, and the interactive HTML statistics report — download-only artifacts built on request, so browsing the grid stays fast.

    Family view

  • Compare — rank the family members on a heat-mapped statistics table and compare any two side by side (models, activity/edge deltas, aligned choice shares).

    Compare view

Plus a Performance overlay sidebar section (frequencies/times on activities and edges, applied to every tab's outputs) with an optional heat-map that colours and thickens activities and edges by value. See docs/model_families.md.

The overlay's default counts are replay-based: they measure how often the log walks each part of the model, so the numbers conserve — an activity's count matches the count on its own edges, parallel branches all carry their fork's inflow, and a choice's branches sum to it. (Event counts and directly-follows counts, still available, measure different things and disagree with each other wherever the model has concurrency or a silently skipped branch.) The view reports how much of the log the model explains alongside them. See docs/metrics.md §9.

Deployed on Streamlit Community Cloud at https://pm4py-ucm.streamlit.app/ (streamlit_app.py, the deployment's main file, is a shim that runs V5, so that URL always tracks the latest code; the earlier V4, V3 and V1 apps live in git history).

https://pm4py-ucm-scenarios.streamlit.app/ deliberately keeps serving the frozen V2 app (web/streamlit_app_v2.py, model + scenarios) — it is referenced by a paper under review and must stay as published.

The Dashboards view — added in V4, and in V5 measured over the same filtered/renamed log as every other view — builds widgets from a metric catalog over your log: filters (including a date range), segmentation, targets with a scorecard that drills into the segments that breached them, and a ƒ custom-formula language. A widget is drawn as a KPI, gauge, histogram, box plot, bar, line, pie/donut or heatmap table — whichever the metric and segmentation actually support — and the grid is drag-reordered and drag-resized. A log can hold several named dashboards, exported one at a time or all in one self-contained interactive HTML file (or as a multi-section session report). See docs/dashboards.md and demo/dashboards_tutorial.ipynb.

Dashboards view

Save, share & resume. A configured session — the log reference, CSV mapping, renaming, filters, performers, overlays, decomposition, family and scenario settings, the open view, and your dashboards — round-trips through a project file, so you can put an analysis down and pick it back up, or hand it to a colleague. Save from the sidebar's Project group as a small settings file (.ucmproj.json, configuration only — no event data) or a self-contained project bundle (.ucmproj.zip, configuration + the log); resume either from the log-source area. Only inputs are stored — the model, scenarios, family and reports recompute on load. See docs/sessions.md.

Export the analysis as runnable Python. The same Project group emits the current session — in one download — as both a plain-Python script (.py, with a CLI entry point) and a Jupyter notebook (.ipynb), reproducing it over the public pm4py_ucm API: log loading, the pre-mining rename + filters, mining, decomposition, performers, the performance overlay (with the heat-map on the exported images, each written as both .png and vector .svg), and optionally the scenario, model-family and dashboards pipelines. The notebook is a personalised tutorial — each stage runs where it's defined and shows the result inline (the mined model and family grid as SVG, the variants, the live dashboards). Because a project stores only inputs, the output is a faithful, deterministic replay — a way to graduate a GUI exploration into an automatable, version-controllable pipeline. It is a template emitter, no LLM involved. See docs/code_export.md.

Run locally with:

pip install -r web/requirements.txt
streamlit run web/streamlit_app_v5.py    # V5 (latest — deployed)
streamlit run web/streamlit_app_v2.py    # V2 (frozen scenarios app)

See web/README.md for the full feature walkthrough and Streamlit Community Cloud deployment instructions.

Why UCM alongside BPMN?

BPMN is excellent for procedural choreographies, but it forces a single abstraction level: every flow object is a step in the process. UCM, by contrast, is a scenario notation. Its elements (path nodes — start points, responsibilities, OR/AND forks and joins, stubs, timers, …) are laid over a backdrop of components, which lets a single map describe a behaviour that crosses architectural boundaries. This makes UCM a natural target when the discovered process tree describes a workflow that spans multiple services or organisational units, or when the goal is requirements engineering rather than execution. See ITU-T Recommendation Z.151 for the full notation reference.

Installation

pip install pm4py-ucm           # core package + graphviz Python bindings
pip install pm4py-ucm[pm4py]    # also install pm4py for discovery
pip install pm4py-ucm[viz]      # add matplotlib for inline notebook display
pip install pm4py-ucm[dev]      # everything (pytest, pm4py, matplotlib)

The graphviz system binary must be on PATH for rendering (the graphviz Python wheel only provides bindings):

# Debian / Ubuntu
sudo apt-get install graphviz
# macOS
brew install graphviz
# Windows
choco install graphviz

Quick tour

Build a UCM by hand

from pm4py_ucm import UCM, write_ucm

ucm = UCM(name="OnlineShop")
m = ucm.add_map(name="ShoppingFlow")

start = m.add_node(UCM.StartPoint(name="start"))
login = m.add_node(UCM.RespRef(resp_def=ucm.get_or_add_responsibility("Login")))
fork  = m.add_node(UCM.OrFork(name="choose"))
search = m.add_node(UCM.RespRef(resp_def=ucm.get_or_add_responsibility("Search")))
browse = m.add_node(UCM.RespRef(resp_def=ucm.get_or_add_responsibility("Browse")))
join   = m.add_node(UCM.OrJoin())
checkout = m.add_node(UCM.RespRef(resp_def=ucm.get_or_add_responsibility("Checkout")))
end    = m.add_node(UCM.EndPoint(name="end"))

m.add_connection(start, login)
m.add_connection(login, fork)
m.add_connection(fork, search, condition="search")    # label on the OR branch
m.add_connection(fork, browse, condition="browse")    # label on the OR branch
m.add_connection(search, join)
m.add_connection(browse, join)
m.add_connection(join, checkout)
m.add_connection(checkout, end)

write_ucm(ucm, "online_shop.jucm")  # open in jUCMNav

Mine a UCM from an event log

import pm4py
import pm4py_ucm

log = pm4py.read_xes("log.xes")
ucm = pm4py_ucm.discover_ucm_inductive(log)
pm4py_ucm.view_ucm(ucm)                                # UCM notation
pm4py_ucm.save_vis_ucm(ucm, "diagram_ucm.png")         # UCM notation
pm4py_ucm.save_vis_ucm(ucm, "diagram_bpmn.png",        # BPMN notation
                       style="bpmn")
pm4py_ucm.write_ucm(ucm, "log.jucm")

discover_ucm_inductive is a thin wrapper around pm4py.discover_process_tree_inductive followed by the bundled process-tree → UCM converter, so all of PM4Py's tuning parameters for the inductive miner are available via the parameters dict.

The PNG renderer supports two visual styles:

  • style="ucm" (default) — Z.151 / jUCMNav notation: filled circle for the start point, perpendicular bar for the end point, × glyph with the responsibility name underneath, thick perpendicular bar for AND-fork / AND-join (synchronisation bars), small filled dot for OR-fork / OR-join, diamond reserved for stubs.
  • style="bpmn" — BPMN-friendly look: activity boxes for responsibilities, gateway diamonds with X / + markers for XOR / AND gateways, thin-border start circle and thick-border end circle (the canonical BPMN end event).

Both styles preserve the swim-lane layout (one rectangle per component, never overlapping unless nested) and wrap long responsibility names onto two or three lines so the diagram stays compact.

.jucm layout matches the PNG layout

When writing a .jucm file, pm4py-ucm uses graphviz's dot engine to compute the coordinates — exactly the same engine that drives the PNG renderer. Open the resulting file in jUCMNav and you'll see the same arrangement of nodes and component rectangles as in the rendered PNG.

If the graphviz binary isn't on PATH, the exporter falls back silently to the bundled Sugiyama-style layouter so writing still works; the visual result will be a layered drawing rather than a graphviz one. Force the built-in layouter explicitly with:

pm4py_ucm.write_ucm(ucm, "log.jucm",
                    parameters={"layout_engine": "builtin"})

Mine performers too — surface them as URN components

If the log records who performed each activity (typically the org:resource or org:role event attribute), pm4py-ucm can mine the activity→performer mapping and use it to populate URN components automatically: each unique performer becomes a ComponentElement, each Responsibility is linked to its performer, and on every map the RespRef symbol for that activity is drawn inside the component's rectangle.

import pm4py
import pm4py_ucm

log = pm4py.read_xes("log.xes")

# One-shot: mine + bind in a single call.
ucm = pm4py_ucm.discover_ucm_inductive(log, parameters={
    "resource_attribute": ["org:role", "org:resource"],  # priority list
})

# Or, the explicit three-step flow with more control.
performers = pm4py_ucm.discover_resources(
    log,
    attribute_priority=["org:role", "org:resource"],
    strategy="mode",        # or "first", "all", "unbound"
    min_support=0.0,        # default — pick the modal performer
                            # even when no single one owns a majority
)
ucm = pm4py_ucm.discover_ucm_inductive(log)
pm4py_ucm.bind_performers(ucm, performers)

pm4py_ucm.write_ucm(ucm, "log.jucm")

Component discovery vs activity binding

There are two related questions to answer when mining resources:

  • Which activities have a clearly-identified performer? This is what discover_resources answers — it returns one performer per activity using the configured aggregation strategy (mode by default). An activity with many performers spread roughly equally gets bound to the modal one; an activity with no org:resource annotation at all is omitted.
  • Which actors exist in the log? This is what discover_components answers — it returns the full vocabulary of every distinct performer value that appears anywhere in the log, sorted. The high-level discover_ucm_inductive calls both, so every distinct actor becomes a URN ComponentElement even when no single responsibility is cleanly bound to it. The unbound components show up in the URN tree but not as rectangles on the map.

This matters for logs with a dispersed resource pool — e.g. the BPI Road Traffic Fines log has 148 distinct org:resource values where the modal performer of Create Fine owns only 5.7% of events. Without these two adjustments (modal binding without majority support, and full component-vocabulary discovery), only one of the 148 actors would appear in the resulting URN spec.

In the rendered diagram and the exported .jucm, each activity now appears inside the rectangle of the team that owns it. See Definitions vs references below for the data-model story.

Read an existing jUCMNav file

from pm4py_ucm import read_ucm

ucm = read_ucm("requirements.jucm")
print(ucm)                          # → UCM(name='…', maps=…, responsibilities=…)
for n in ucm.maps[0].nodes:
    print(n)

Process-tree → UCM mapping

The converter implements the following correspondences between PM4Py's process tree operators and UCM constructs. UCM has no native loop, so loops are encoded as an OR-fork/OR-join pair guarded by [redo] / [exit] conditions — the canonical idiom in jUCMNav.

Process tree UCM construct
Activity leaf with label A RespRef referencing a Responsibility named A
Silent (τ) leaf A direct NodeConnection with no responsibility
→ (sequence) of children Children chained with EmptyPoint connectors (collapsed by simplifier)
× (xor) choice OrFork → branches → OrJoin
+ (parallel) AndFork → branches → AndJoin
o (interleaving) Treated as + (parallel)
∨ (or) Treated as × (xor)
↻ (loop, do, redo) OrJoindoOrFork with [redo] back-edge and [exit] forward edge

After conversion, an EmptyPoint simplification pass collapses chains of unnamed degree-2 connectors so that the resulting map renders compactly.

Hierarchical decomposition

For complex process trees, a single flat UCM map quickly becomes visually overwhelming. The optional decomposition= keyword on discover_ucm_inductive and convert_to_ucm splits the result into a root map plus plug-in (sub-)maps connected by Stubs. The same PluginBinding machinery the package already uses for hand-built models ties everything together — every round-trip stays byte-stable through the exporter and importer.

import pm4py
import pm4py_ucm

log = pm4py.read_xes("running-example.xes")

# Default: one flat map (current behaviour, byte-stable with old exports)
flat = pm4py_ucm.discover_ucm_inductive(log)
assert len(flat.maps) == 1

# Hierarchical: a root map + one plug-in per "phase" / branch / loop body
hier = pm4py_ucm.discover_ucm_inductive(log, decomposition="auto")
assert len(hier.maps) >= 1

pm4py_ucm.view_ucm(hier)                   # all maps stacked in one PNG
pm4py_ucm.view_ucm(hier, map="loop_Test")  # just one plug-in
pm4py_ucm.write_ucm(hier, "out.jucm")      # opens in jUCMNav as root+plug-ins

The decomposition argument accepts:

Value Effect
None / "off" No decomposition. Output byte-stable with pre-decomposition exports.
"auto" All four boundary rules on; max_leaves_per_map and min_leaves_to_decompose fitted to the tree shape at conversion time — see suggest_decomposition (cap ≈ 1.5·√N, floor ≈ 0.15·N in the leaf count N).
"aggressive" Fixed preset: same boundary rules, max_leaves_per_map=10.
dict Any subset of the keys below; unspecified keys take the "auto" defaults. A size dimension may be the string "auto" to fit just that one to the shape.

pm4py_ucm.suggest_decomposition(process_tree) returns the shape-fitted dict directly if you want to inspect or tweak it.

Configurable keys:

Key Default Meaning
on_root_sequence True Each child of a top-level becomes a plug-in. Root map reads as a chain of phase stubs.
on_parallel True Each + branch becomes a plug-in. AND-fork/join vertical-expansion cost is replaced by a single stub per branch.
on_alternative True Each × (XOR) / (OR) branch becomes a plug-in. OR-fork/join stays on the parent map; alternative bodies move into per-branch plug-ins.
on_loop True Each * operator's entire expansion becomes a plug-in. Parent map reads as forward flow with one stub for the iteration. A loop at the root of the tree is wrapped in a synthetic sequence so the root map gets a single loop stub.
max_leaves_per_map 20 Hard cap; over-sized maps recursively force-cut the largest operator-subtree until the cap is met.
min_leaves_to_decompose 4 Floor — subtrees smaller than this stay inlined regardless of rules.
balance_ratio 0.2 Sibling share threshold under and +. A child needs at least this fraction of the parent's leaves to be pulled out independently.

Unknown keys raise ValueError.

When the UCM has multiple maps, view_ucm and save_vis_ucm compose every map vertically into a single PNG (Pillow does the stacking). Each panel has a title strip and adjacent panels are separated by a thin horizontal rule. Bound stubs gain a → <plug-in name> external label so the reader can follow each stub to its plug-in map.

Scenario synthesis

The discover_scenarios pipeline turns an event log into an executable UCM: a .jucm carrying one URN ScenarioDef per behavioural variant discovered in the log, with typed variables, per-loop integer counters, and mutually-exclusive OR-fork conditions that let jUCMNav step through each scenario deterministically.

import pm4py
import pm4py_ucm

log = pm4py.read_xes("log.xes")

# Variant-driven (default) — lossless replay of every observed variant
ucm, clustering = pm4py_ucm.discover_scenarios(log)
pm4py_ucm.write_ucm(ucm, "log.jucm")
pm4py_ucm.write_variants_report(clustering, "variants.csv")
pm4py_ucm.write_case_variant_map(clustering, "case_variant_map.csv")

# Data-driven — mine per-fork decision trees over case attributes,
# emit conditions like `Broker == Spot_Health_Insurance && Claim_Value <= 1417646`
ucm_dd, _ = pm4py_ucm.discover_scenarios(
    log, condition_strategy="data-driven",
    decision_tree_max_depth=3,
)
group = ucm_dd.scenario_groups[0]
pm4py_ucm.write_ucm(ucm_dd, "log.data_driven.jucm")
pm4py_ucm.write_condition_mining_report(group, "condition_mining.csv")

What the synthesizer populates

  • EnumerationType VariantId with values [v1, v2, …] (variant-driven only), plus one EnumerationType per case-constant string attribute the log carries (data-driven only).
  • Variables — a variant_id enum (variant-driven) or one variable per mined case attribute (data-driven), plus one integer per loop operator in the discovered tree (contextually named e.g. Loop_AnalyzeClaim).
  • One ScenarioDef per variant, each with an Init per variable (variant-driven initialises variant_id; data-driven initialises each attribute to a representative value for its variant — mode for enum/bool, scaled median for integer), plus a per-loop counter init capped at max_loop_iterations (default 2), plus ScenarioStartPoint / ScenarioEndPoint refs. Names carry a short discriminator (v3_TwoCloseAssessmen, v8_QuickAssessment); descriptions start with a plain-English Intent: line.
  • Arc conditions on every non-loop OR-fork — variant-driven writes variant_id == v_i disjunctions (with an inside-loop variant that combines the disjunction with counter thresholds); data-driven writes mined boolean expressions over case attributes.
  • A LoopEntryGuard OR-fork per loop, spliced between the loop's upstream arc and its LoopJoin, with mutually-exclusive counter > 0 / counter <= 0 conditions. This restores the semantics "counter = number of body executions" — including zero. When a loop's post-loop continuation is a Stub, an OrJoin is spliced before the stub so its plug-in binding stays complete.

Before running scenarios in jUCMNav: check the hit-count ceiling

jUCMNav declares an infinite loop as soon as a single element has been entered as many times as its maximum hit count preference allows (Preferences → jUCMNav → Scenario Traversal). That ceiling is a setting, not a property of your model. Set below what a model legitimately needs, jUCMNav abandons the offending visit, its AND-joins then starve, and the scenario never reaches its end point — producing a Problems view full of blocked joins and unreached end points for a model that is perfectly correct.

A model whose discovered tree nests loops around parallel branches enters some elements once per iteration of every enclosing loop, so it can need a far higher ceiling than a flat one. Across the 28 models of our evaluation most need 2–6; one, a depth-3 loop nest, needs 10 — and against a preference set to exactly 10 it reported eight errors that were entirely artefacts of the setting.

jUCMNav's own default is 1000, which is ample. If yours has been lowered, raise it before concluding that a generated model is broken. To find what a specific model needs:

from pm4py_ucm.algo.scenario_traversal import required_max_hit_count

need, scenario, element = required_max_hit_count(ucm)
print(f"set jUCMNav's maximum hit count to at least {need} "
      f"(driven by {element} in {scenario})")

The same module executes every scenario offline under jUCMNav's traversal semantics, so a model can be checked before it is opened in the tool at all — it reports blocked AND-joins, unreached end points, and forks where no branch (or more than one) is enabled:

from pm4py_ucm.algo.scenario_traversal import check_traversal

for problem in check_traversal(ucm):
    print(problem)

Concurrency-aware variants

Two traces that differ only in the interleaving order of activities inside a parallel block share the same choice signature and therefore the same variant. X → (Y ∥ Z) → W traces X-Y-Z-W and X-Z-Y-W cluster as one; sequence-variant analysis splits them. The compression_ratio (concurrency-aware / sequence-variant count) on ClaimsPaymentLog is 0.146, meaning naive clustering over-counts by ~7×.

Loop iteration counts are coarsened to {0, 1, ≥2} by default to keep the variant count small; pass coarsen_loops=False to distinguish every iteration count.

Reading a variant expression

Each variant is summarised as a compact partial-order expression (shown in variants.csv, the Scenarios tab, and scenario descriptions). It is a per-variant projection of the discovered process tree — it shows what this behavioural variant actually did, not the whole model:

Syntax Source Meaning
A activity leaf activity A was executed here — no choice, no loop
X -> Y sequence X, then Y
[A] XOR / OR a choice point; branch A is the one this variant took (another variant may show [B] here)
(X || Y) AND (parallel) X and Y ran concurrently — any interleaving is the same variant
A^0 / A^1 / A^>=2 loop loop body A ran zero / exactly once / two-or-more times (the coarsened {0, 1, ≥2} bucket)

Two conventions worth knowing: a parallel branch this variant skipped shows as [tau] (the silent choice was taken), so (E || [tau]) means "E, in parallel with an optional branch that was skipped here"; and a parallel of a single activity with nothing (A || tau) simplifies to just A.

Two condition-encoding strategies

Strategy Arc conditions Trade-off
variant variant_id == v_i disjunctions per branch Lossless — replaying scenario v_i reproduces v_i's choice signature exactly. Doesn't explain choices.
data-driven Boolean expressions over case attributes, mined per-fork by decision trees Business-readable rules on every fork; requires case-constant attributes; abandons with a warning otherwise.

Inside-loop OR-forks (XORs sitting inside a loop body): variant-driven combines variant_id with the enclosing counter to distribute branches across iterations. Data-driven falls back, only for inside-loop forks, to a deterministic true/false split — case attributes are static per case and can't disambiguate per-iteration choices.

Reports

Three CSVs alongside the .jucm:

  • variants.csv — one row per variant with frequency, sequence- variant count, linearization count, partial-order expression, and a truncated case-ID list. Trailing rows for noise and totals (fitness + compression).
  • case_variant_map.csv — one row per case, mapping case ID to variant ID (or noise for non-conforming cases).
  • condition_mining.csv — data-driven mode only. One row per (OR-fork, branch) with accuracy, sample size, feature set, skipped_reason (inside_loop, no_labelled_cases), and the post-minimisation expression emitted on the arc.

Interaction with decomposition

discover_scenarios accepts the same decomposition= argument as discover_ucm_inductive and honours it fully: OR-forks that land in plug-in maps receive the same conditions they would in the flat case, and loops pushed into plug-in maps get their counter machinery (LoopEntryGuard, decrement responsibility) spliced into the correct map. Each UCM OrFork / OrJoin / LoopFork / LoopJoin carries a stable id linking it back to the tree node it came from, so correlation survives arbitrary decomposition boundaries.

Learning path

Model families (attribute-partitioned discovery)

Many logs mix cases that follow different processes — a cancer-care log contains distinct pathways per cancer type, a claims log may route work differently per country. discover_ucm_family partitions the log by the values of one or two case-level attributes and mines one model per combination; the family can then be exported as separate models, rendered side by side, or assembled into a single overarching model where UCM's own variability constructs carry the family: dynamic stubs are variation points, plug-in maps are variants, and scenario strategies are configurations.

family = pm4py_ucm.discover_ucm_family(
    log, ["cancer_type", "age"],       # 1–2 attributes; numerics are binned
    decomposition="auto", min_cases=20,
)

pm4py_ucm.write_ucm_family(family, "family.zip")        # one .jucm per cell
pm4py_ucm.save_vis_ucm_family(family, "grid.png")       # stack / matrix view

umbrella = pm4py_ucm.assemble_ucm_family(family, mode="umbrella")
pm4py_ucm.write_ucm(umbrella, "family_umbrella.jucm")   # opens in jUCMNav

The umbrella's root map is the shared skeleton of the cell processes (computed by anti-unifying the per-cell process trees), with a dynamic stub only where behaviour actually diverges; each stub's plug-ins are guarded by preconditions over the attributes (cancer_type == Breast && age_group == _40_59). Behaviourally identical variants share one plug-in with a domain-factored condition; resource variation counts as variation (the same activity done by different actors becomes a variation point, each variant drawn inside its actor); and by default each combination gets executable path scenarios — one per behavioural variant of its sub-log, with family_variant branch conditions and loop counters, so jUCMNav's traversal walks genuinely different paths per strategy.

Full documentation — partitioning policy, skeleton merge rules, dedup/conditions, path scenarios, grid resolution, value filtering — in docs/model_families.md. The web app's Family view exposes all of it interactively, including a pre-mining coverage heatmap and per-attribute value filters.

Family statistics reports

Compare the family's processes quantitatively — and hand the result to collaborators as one self-contained interactive HTML file:

stats = pm4py_ucm.compute_family_stats(family)     # needs family.log_df
stats.process_frame()                              # pandas: one row per cell
pm4py_ucm.write_family_report(family, "report.html", stats=stats)

compute_family_stats yields four statistics levels per family member: process (cases, events per case, case-duration min/mean/median/max/total/std/P90/P95, rework rate, start/end activity distributions, behavioural variant counts, replay fitness), activity (frequency, relative frequency, case coverage, repeat frequency, sojourn time since the previous event — available even for single-timestamp logs — and service-time min/mean/median/max/std/P90/ P95/total on interval logs), edge (directly-follows pairs with traversal frequency, case frequency, relative frequency, and waiting-time aggregates), and choice — OR-fork branch counts aligned across the family through the shared skeleton, so the same decision point is one comparable row for every combination. Every metric is defined in docs/metrics.md.

write_family_report renders it all into a zero-dependency HTML report that opens offline in any browser: sortable heat-mapped ranking tables, a pair-comparison view (any two members side by side with their model images embedded, delta cards, activity Δ/ratio tables), 100% stacked branch-share bars per choice, and a model gallery. Every share and time is shown next to its n, and metrics the log cannot support are omitted rather than faked. The web app's Compare view serves the same statistics interactively and offers the report as a download.

Performance overlays

Frequencies and times computed from the log, displayed on activities and edges and exported as jUCMNav metadata:

pm4py_ucm.annotate_performance(
    ucm, log,
    node_metrics=["frequency", "median_time"],
    edge_metrics=["percentage", "mean_time"],
)
pm4py_ucm.save_vis_ucm(ucm, "annotated.png")   # small gray overlay text
pm4py_ucm.write_ucm(ucm, "annotated.jucm")     # per-metric metadata lines

Activity metrics: frequency, case_coverage, relative_frequency, repeat_frequency (rework), and (for interval logs with a start_timestamp column) mean/median/min/max/std/p90/p95/total_time service times, plus the sojourn_* variants on any timestamped log. Edge metrics: directly-follows frequency, case_frequency, relative_frequency, OR-fork branch percentage, and the mean/median/min/max/std/p90/p95/total_time waiting times — attributed via activity-to-activity segments that walk through bends, joins, forks, and static stubs (so decomposed models are covered too). The sidebar's ≤2 selections control what is drawn on the diagram, but every available metric is exported as its own perf_<metric> metadata line for jUCMNav's properties view; the family assemblies annotate the shared skeleton from the whole log and each variant plug-in from its own sub-log. Overlay walkthrough in docs/model_families.md.

Beyond the text sub-line, an optional heat-map colours and thickens activities and edges by the first chosen metric of each layer — across the Model, Family and Compare views alike (a time metric reads red, any other blue; darker/thicker = higher). Its scale control offers three modes: Local (per map) (each diagram to its own min/max), Per family member (across its maps) (each Family/Compare member against its own range, pooled over its decomposed maps — the whole model in the Model view), and Global (across family members) (every member against one shared range, so a colour is comparable across members). It is a render-time overlay of the same perf_<metric> values — the model and .jucm are unchanged — exposed by model_to_svg(ucm, style, heatmap=True, node_metric=…, edge_metric=…, heatmap_global=…, node_span=…, edge_span=…); classic.heat_span(models, …) computes the shared cross-member span.

📏 docs/metrics.md is the readable, complete list of every metric definition — activity, edge, process and choice — with units, timestamp semantics, aggregation rules, and the deliberate edge-case decisions (negative waiting on overlapping intervals, tie handling, and the working-calendar caveat: all times are raw wall-clock, with no weekends/holidays subtracted). It is the semantic contract the metric-validation suite enforces against pm4py.

Module layout

pm4py_ucm/
├── api.py                                 # high-level read_/write_/discover_/view_
├── objects/ucm/
│   ├── obj.py                             # UCM object model (URN metamodel)
│   ├── conversion/from_process_tree.py    # PM4Py process tree → UCM
│   ├── conversion/decomposition.py        # hierarchical decomposition rules + presets
│   ├── exporter/variants/jucm.py          # UCM → jUCMNav .jucm (XMI 2.0)
│   ├── importer/variants/jucm.py          # jUCMNav .jucm → UCM
│   └── layout/layouter.py                 # auto-layout for jUCMNav graphical view
├── algo/
│   ├── performance.py                     # frequency/time overlays on activities + edges
│   └── discovery/
│       ├── ucm/
│       │   ├── algorithm.py               # discovery dispatcher (mirrors BPMN)
│       │   └── variants/inductive.py      # inductive-miner-based discovery
│       ├── variants/                      # concurrency-aware variant clustering
│       │   ├── choice_signature.py        # replay algorithm + signature canonicalisation
│       │   └── clustering.py              # per-variant clustering + fitness / compression
│       ├── scenarios/                     # scenario synthesis on top of a UCM + clustering
│       │   ├── synthesis.py               # variables, ScenarioDefs, LoopEntryGuard, conditions
│       │   ├── decision_mining.py         # data-driven strategy: sklearn tree → jUCMNav expr
│       │   ├── expression_minimizer.py    # boolean simplifier for mined expressions
│       │   └── reports.py                 # variants.csv / case_variant_map.csv / condition_mining.csv
│       └── families/                      # attribute-partitioned model families
│           ├── partition.py               # case-attribute detection + log partitioning
│           ├── family.py                  # ModelFamily container + zip/dir export
│           ├── algorithm.py               # per-cell discovery driver
│           ├── assembly.py                # combined + skeleton-umbrella assembly
│           ├── scenarios.py               # per-cell path scenarios on the umbrella
│           ├── stats.py                   # FamilyStats: process/activity/choice comparison
│           └── report.py                  # self-contained interactive HTML report
└── visualization/ucm/
    ├── visualizer.py                      # apply / view / save (mirrors BPMN)
    ├── stacked.py                         # vertical multi-map composition
    ├── family_grid.py                     # family stack/matrix rendering (adaptive DPI)
    └── variants/classic.py                # graphviz-based renderer

The object model in objects/ucm/obj.py mirrors the jUCMNav EMF metamodel (urn, urncore, ucm.map) closely enough to emit XMI files that load directly in jUCMNav.

Definitions vs references

For the full Python object model in one picture, see docs/ucm_class_diagram.svg (vector, paper-ready) or the PNG preview. The PlantUML source can be re-rendered or extended for figures in academic papers.

UCM keeps a sharp distinction between a definition (a reusable named concept declared once at the URN level) and each visual reference to it on a map. The object model surfaces this distinction explicitly:

Definition (one) Reference (many) Where reference lives
UCM.Responsibility (an activity) UCM.RespRef (the "✕" symbol) inside a map's nodes list
UCM.ComponentElement (an actor / team / role / system) UCM.ComponentRef (the labelled rectangle) inside a map's cont_refs list

A definition lives on the URN container (ucm.responsibilities, ucm.components). Each visual occurrence on a diagram is a separate reference object that points back to the definition via resp_def (for RespRef) or cont_def (for ComponentRef). Many references may share the same definition — that is exactly what lets the same activity or the same actor appear in multiple places without being declared twice.

A third link — Responsibility.performer — runs between definitions: it expresses the semantic fact that a given activity is performed by a given actor/team. This is a logical binding, independent of layout. The visual binding (RespRef.cont_ref → ComponentRef) is derived from it whenever a map is built or UCM.bind_performers() is called.

─ URN level ──────────────────────────────────────────
 Responsibility "Login" ──performer──> ComponentElement "AuthService"
            ▲                                  ▲
            │ resp_def                         │ cont_def
─ Map level ──────────────────────────────────────────
   RespRef #5  ─────cont_ref─────────>  ComponentRef #12
   (drawn as ✕)                         (drawn as a rectangle)

Build them through the helpers:

from pm4py_ucm import UCM

ucm = UCM(name="Example")
m   = ucm.add_map(name="MainMap")

# --- DEFINITIONS (one per concept, declared on the URN container) ---
login_def = ucm.get_or_add_responsibility("Login")          # Responsibility
actor_def = ucm.get_or_add_component(                       # ComponentElement
    "User", kind=UCM.ComponentElement.Kind.ACTOR)

# --- REFERENCES (many per definition, drawn on the map) -------------
login_node = m.add_node(UCM.RespRef(resp_def=login_def))    # RespRef
actor_box  = m.add_component_ref(actor_def, width=200, height=120)

# A path node may declare which component reference visually contains it:
login_node.cont_ref = actor_box

In the exported .jucm you'll see this distinction reflected as the bidirectional links jUCMNav uses internally:

<responsibilities name="Login" id="35" respRefs="36"/>
<components       name="User"  id="45" contRefs="46" kind="Actor"/><nodes   xsi:type="ucm.map:RespRef" id="36" respDef="35" contRef="46" />
<contRefs xsi:type="ucm.map:ComponentRef" id="46" contDef="45" nodes="36" />

The respRefs/contRefs attributes on a definition list back-references to every occurrence of it; respDef/contDef on the reference point forward to the definition. The exporter computes the back-references automatically — you only need to set the forward links.

Compatibility with jUCMNav

The exporter produces files in the modern jUCMNav format (matches output of jUCMNav 5.5 and later):

  • Root element <urn:URNspec> declares four namespaces — xmi, xsi, urn (http:///urn.ecore), and ucm.map (http:///ucm/map.ecore). The urncore and grl packages do not need declarations because their concepts use unqualified element names inside the URN containment tree.
  • URN-level metadata (urnVersion="1.27", specVersion="4", name, author, created, modified, nextGlobalID) lives on the root as attributes, not child elements.
  • Children of <urn:URNspec> appear in the canonical order ucmspec → grlspec → urndef. Children of <urndef> appear in the order responsibilities → specDiagrams → components. Children of a <specDiagrams> UCMmap appear as nodes → contRefs → connections.
  • Connections are anonymous<connections> carries no id attribute. Endpoints use integer node IDs (source="18" target="19").
  • Nodes refer to their connections via XPath fragments (succ="//@urndef/@specDiagrams.0/@connections.7"), the only style available because connections lack IDs.
  • <condition> elements distinguish a human-readable label (e.g. "TrueBranch") from the logical expression (default "true").
  • nextGlobalID is exactly max(all IDs) + 1 — the integer ID jUCMNav would assign to the next newly-created element.
  • An auto-layouter places nodes on a layered left-to-right grid before export so the diagram is immediately readable in jUCMNav's editor.

Documentation

The full API reference — every public helper and every module, rendered from the in-source docstrings — is published to GitHub Pages:

https://processmining-uottawa.github.io/pm4py-ucm/

It is built with pdoc and redeployed automatically on every push to main (.github/workflows/docs.yml); nothing generated is committed. To reproduce the deployed site locally:

pip install -e .[docs]
pdoc pm4py_ucm pm4py_ucm.algo pm4py_ucm.api pm4py_ucm.objects \
     pm4py_ucm.util pm4py_ucm.visualization -o docs/api
# then open docs/api/index.html

The landing page is the curated public API (the names in pm4py_ucm.__all__); naming the subpackages makes pdoc expand each into its full module tree, which the sidebar links. pdoc pm4py_ucm on its own documents only the public-API page, because __all__ lists just the public helpers. Narrative guides live alongside the code in docs/: metrics.md, dashboards.md, model_families.md.

Testing

pip install -e .[dev]
python -m unittest discover -s tests -v

See tests/README.md for a per-file map of the suite (purpose and test count for each of the 22 test modules).

The default test suite does not require PM4Py to be installed: the process-tree → UCM converter accepts duck-typed trees (operator.value, children, label), which the tests use to exercise every operator in isolation. The tests/test_export_import.py suite verifies that round-trip through the jUCMNav XMI back-end is byte-deterministic.

The performance and family-statistics metrics are validated in tests/test_metric_validation.py against four independent oracles — a hand-computed distinct-value fixture, algebraic invariants, metamorphic transforms, and simulation ground truth — with an optional differential check against pm4py's own DFG/duration functions. The metrics they enforce are specified in docs/metrics.md.

Security scan

A lightweight static scan runs locally — no repository code-scanning or CI minutes required:

bandit -r pm4py_ucm web -ll     # medium+ severity; ships in .[dev]

The medium/high baseline is clean. The .jucm importer refuses DTDs before parsing (_forbid_dtd) so untrusted .jucm input cannot trigger XML entity-expansion ("billion laughs") — a zero-dependency alternative to defusedxml. Event-log (XES) parsing is delegated to PM4Py.

License

GPL-3.0-or-later, matching the upstream PM4Py license. The jUCMNav metamodel reproduced (in spirit) here is itself distributed under EPL-2.0; this package only re-implements the metamodel in Python and does not bundle any jUCMNav source.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pm4py_ucm-0.7.10.tar.gz (409.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pm4py_ucm-0.7.10-py3-none-any.whl (404.6 kB view details)

Uploaded Python 3

File details

Details for the file pm4py_ucm-0.7.10.tar.gz.

File metadata

  • Download URL: pm4py_ucm-0.7.10.tar.gz
  • Upload date:
  • Size: 409.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pm4py_ucm-0.7.10.tar.gz
Algorithm Hash digest
SHA256 6f79084e992fd249557bceefe612e620ef38425c0d7e5edf257ca2f09db09c8b
MD5 76247b2b7f0530661bd65445eb010f02
BLAKE2b-256 dc525a08b0a594aa20add8aed6d08f5997c4507f872b4817268df25e09a3dcf1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pm4py_ucm-0.7.10.tar.gz:

Publisher: publish.yml on ProcessMining-uOttawa/pm4py-ucm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pm4py_ucm-0.7.10-py3-none-any.whl.

File metadata

  • Download URL: pm4py_ucm-0.7.10-py3-none-any.whl
  • Upload date:
  • Size: 404.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pm4py_ucm-0.7.10-py3-none-any.whl
Algorithm Hash digest
SHA256 ca6fac41b19038ce454281d6e39611ff630ae5f016b4b47906f8962c0db77ba4
MD5 ccfb6519b9929e9e1d4bf242b37fdc8c
BLAKE2b-256 25ce36a9adff8093d556c61f76f3224dcf387193b5556139608671b302d6426f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pm4py_ucm-0.7.10-py3-none-any.whl:

Publisher: publish.yml on ProcessMining-uOttawa/pm4py-ucm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page