pm4py-ucm
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/.svgexport, performer mining, hierarchical decomposition,.jucmround-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.ipynb— save, share & resume a project: the config-vs-data-vs-derived principle, the parameter registry andcollect, a settings file vs a self-contained project bundle, the versioned dashboards envelope,loadround-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.jucmfile, 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. -
Scenarios — concurrency-aware variant clustering + one executable jUCMNav
ScenarioDefper 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.jucmwith the synthesized scenario group,variants.csv,case_variant_map.csv, and (data-driven mode)condition_mining.csv. Runs on flat and decomposed UCMs alike. -
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. -
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).
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.
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 withX/+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_resourcesanswers — it returns one performer per activity using the configured aggregation strategy (modeby default). An activity with many performers spread roughly equally gets bound to the modal one; an activity with noorg:resourceannotation at all is omitted. - Which actors exist in the log? This is what
discover_componentsanswers — it returns the full vocabulary of every distinct performer value that appears anywhere in the log, sorted. The high-leveldiscover_ucm_inductivecalls both, so every distinct actor becomes a URNComponentElementeven 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) |
OrJoin → do → OrFork 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
EnumerationTypeVariantIdwith values[v1, v2, …](variant-driven only), plus oneEnumerationTypeper case-constant string attribute the log carries (data-driven only).Variables — avariant_idenum (variant-driven) or one variable per mined case attribute (data-driven), plus oneintegerper loop operator in the discovered tree (contextually named e.g.Loop_AnalyzeClaim).- One
ScenarioDefper variant, each with anInitper variable (variant-driven initialisesvariant_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 atmax_loop_iterations(default 2), plusScenarioStartPoint/ScenarioEndPointrefs. Names carry a short discriminator (v3_TwoCloseAssessmen,v8_QuickAssessment); descriptions start with a plain-EnglishIntent:line. - Arc conditions on every non-loop OR-fork — variant-driven writes
variant_id == v_idisjunctions (with an inside-loop variant that combines the disjunction with counter thresholds); data-driven writes mined boolean expressions over case attributes. - A
LoopEntryGuardOR-fork per loop, spliced between the loop's upstream arc and itsLoopJoin, with mutually-exclusivecounter > 0/counter <= 0conditions. This restores the semantics "counter = number of body executions" — including zero. When a loop's post-loop continuation is aStub, anOrJoinis 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 fornoiseandtotals(fitness + compression).case_variant_map.csv— one row per case, mapping case ID to variant ID (ornoisefor 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
demo/scenario_synthesis_tutorial.ipynb— a small synthetic example per section, then a capstone onClaimsPaymentLog(24 variants, compression 0.146) in both the variant-driven and data-driven / decision-mining encodings.- The Scenarios view in
web/streamlit_app_v5.py— no code needed.
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), anducm.map(http:///ucm/map.ecore). Theurncoreandgrlpackages 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 noidattribute. 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-readablelabel(e.g."TrueBranch") from the logicalexpression(default"true").nextGlobalIDis exactlymax(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:
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f79084e992fd249557bceefe612e620ef38425c0d7e5edf257ca2f09db09c8b
|
|
| MD5 |
76247b2b7f0530661bd65445eb010f02
|
|
| BLAKE2b-256 |
dc525a08b0a594aa20add8aed6d08f5997c4507f872b4817268df25e09a3dcf1
|
Provenance
The following attestation bundles were made for pm4py_ucm-0.7.10.tar.gz:
Publisher:
publish.yml on ProcessMining-uOttawa/pm4py-ucm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pm4py_ucm-0.7.10.tar.gz -
Subject digest:
6f79084e992fd249557bceefe612e620ef38425c0d7e5edf257ca2f09db09c8b - Sigstore transparency entry: 2480857525
- Sigstore integration time:
-
Permalink:
ProcessMining-uOttawa/pm4py-ucm@64a0437edaacd8e0b0125ccddad29ce0ff2fcee4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ProcessMining-uOttawa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@64a0437edaacd8e0b0125ccddad29ce0ff2fcee4 -
Trigger Event:
workflow_dispatch
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca6fac41b19038ce454281d6e39611ff630ae5f016b4b47906f8962c0db77ba4
|
|
| MD5 |
ccfb6519b9929e9e1d4bf242b37fdc8c
|
|
| BLAKE2b-256 |
25ce36a9adff8093d556c61f76f3224dcf387193b5556139608671b302d6426f
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pm4py_ucm-0.7.10-py3-none-any.whl -
Subject digest:
ca6fac41b19038ce454281d6e39611ff630ae5f016b4b47906f8962c0db77ba4 - Sigstore transparency entry: 2480857614
- Sigstore integration time:
-
Permalink:
ProcessMining-uOttawa/pm4py-ucm@64a0437edaacd8e0b0125ccddad29ce0ff2fcee4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ProcessMining-uOttawa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@64a0437edaacd8e0b0125ccddad29ce0ff2fcee4 -
Trigger Event:
workflow_dispatch
-
Statement type: