Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,80 @@ and each [release on GitHub](https://github.com/s4mstruthers/openprocess/release
uses it as its description. Write for the people using the app: what they will
notice, not how it was done. Versions before 0.7.0 were released as CPNpy.

## 0.10.5

- **Object-centric event logs.** *Open object-centric log* reads OCEL 2.0
JSON (`.jsonocel`) and the older JSON-OCEL: events that each involve
several objects, by type, with no case id invented. *Flatten on an object
type* makes a classical log with one case per object of a type, for the
miners and checks that need one, and says what it copied. *Object-centric
map* draws the object-centric directly-follows graph: the activities
once, one set of arrows per object type in its own colour, with a
legend and the counts per type. A `.jsonocel` in the folder is listed
under LOGS, drops on the canvas, and appears in the Summary and the
report like any other result.

## 0.10.4

- **A command palette.** ⌘K (Ctrl+K), or *View ▸ Command Palette…*: type a
few letters and the open analyses, models and logs, the folder's files,
every box (added to the current analysis), the three spaces, Connections
and every menu action are found at once; Enter runs the best match, ↑ ↓
pick another. Nothing new on the window; everything in it a few
keystrokes away.

## 0.10.3

- **Runs: every finished run, kept.** An analysis has *Canvas | Summary |
Runs*. Each finished run is a snapshot with the time, what changed before
it ("Inductive Miner: noise 0 → 0.2", "+ Check fit", "Run again, same
results") and every box's key figure. Pick A and B and the boxes line up
side by side, those whose result differs marked. *Use A's settings* puts
that run's settings back on the canvas and runs. The last twenty travel
with the workflow file.

## 0.10.2

- **Export report.** ⋯ ▸ *Export report…* on an analysis writes one HTML
page, with nothing to install: the reproducibility status, the key figure
of every box, each box's result (nets, maps and trees drawn; tables and
scores as tables), how it got there, the code it ran and the papers it
follows, what it read with fingerprints, and how to cite. For a
supervisor, a reviewer, a hand-in. It opens in your browser when written.

## 0.10.1

- **Step through an algorithm.** A box's *How* tab has *Step through ▸*: the
report plays one moment at a time (a note, a step of the derivation, a
value shown), with ◀ ▶, Play and the arrow keys, and the box's result drawn
above it. Each step lights up on the drawing what it names and dims the
rest, so the α-algorithm's eight steps become eight pictures: all the
activities, the start ones, the end ones, the candidate pairs, the maximal
pairs, the places. Nothing new is computed: the moments are the box's own
report, in order.

## 0.10.0

Two things an academic looks for first: is this reproducible, and how do I
cite it.

- **A reproducibility badge beside every analysis's title.** "Not recorded
yet"; "Recorded · this run reproduces it" when every result matches the
record's fingerprints and nothing the analysis depends on has changed;
"Recorded · n changes since" when an input file, a box file, a seed or a
version differs (hover for which); "This run differs from the record"
when the same inputs gave another result. Click it for the record. In a
folder the record follows each run by itself.
- **Cite.** *Help ▸ Cite OpenProcess…* gives the app's BibTeX and a one-line
citation. On a box's Code tab, each algorithm has *Cite* with BibTeX for
the paper it follows, generated from the reference list. An exported
experiment includes `CITATIONS.bib` and a *How to cite* section. The
repository has a `CITATION.cff`, so GitHub offers *Cite this repository*.
- **Fixed: an input file edited on disk was served from the cache.** A box
reading a file is now keyed by the file as it is (size and time), so a
re-run after editing the log computes again instead of repeating the old
result.

## 0.9.5

- **The log page no longer discovers models by itself.** Its *Discover* tab
Expand Down
24 changes: 24 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
cff-version: 1.2.0
message: If you use OpenProcess in teaching or research, please cite it as below.
type: software
title: OpenProcess: process mining, Petri nets and workflows in one open app
version: 0.10.5
date-released: 2026-10-10
repository-code: https://github.com/s4mstruthers/openprocess
url: https://github.com/s4mstruthers/openprocess
license: MIT
keywords:
- process mining
- Petri nets
- workflow nets
- conformance checking
- teaching
- reproducible research
authors:
- family-names: Struthers
given-names: Sam
abstract: >-
OpenProcess is a desktop app for process mining and Petri net modelling in which
every algorithm is a function whose code and source paper are one click away, and
every analysis keeps a record of its inputs, versions and seeds so that its numbers
can be checked again.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ tools (and, until version 0.7, went by the name CPNpy):
| | What you can do | Instead of |
|---|---|---|
| **Petri nets & WF-nets** | Draw nets the way the lectures do. Get a soundness verdict with a counterexample for every violation, and replay it in the token game. Also: behavioural properties, P- and T-invariants, the footprint matrix, the reachability graph, PNML import and export. | WoPeD, ProM, pen and paper |
| **Process mining** | Import XES or CSV logs, or type textbook logs like `[<a,b,c>^3, <a,c>^2]`. Filter them, explore variants, the dotted chart and the process map. Discover models (α-algorithm, Inductive Miner, Heuristics Miner, state-based regions). Check conformance (token replay, alignments, precision…) and compare logs. | ProM, Disco |
| **Process mining** | Import XES or CSV logs, OCEL 2.0 object-centric logs, or type textbook logs like `[<a,b,c>^3, <a,c>^2]`. Filter them, explore variants, the dotted chart and the process map. Discover models (α-algorithm, Inductive Miner, Heuristics Miner, state-based regions). Check conformance (token replay, alignments, precision…) and compare logs. | ProM, Disco |
| **Coloured Petri nets** | Open, edit and save CPN Tools models (`.cpn`), hierarchical ones included. Step through or simulate them, compute the state space, and export a simulation as an event log to mine. | CPN Tools / CPN IDE |
| **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.cpnflow` file with everything needed to get the same numbers back. Your own algorithm is one Python function in a `boxes/` folder. | RapidProM |
| **Learn** | A mode of its own for worksheets, built on the app: answer in boxes on the sheet (sets, markings, matrices, cuts and trees, alignments, a net in the editor or a workflow on the canvas beside it) and press **Check**. Most answers are checked automatically, often against answers worked out from the given log or net. Packs are plain Markdown, can be exams with a clock and points, and a past exam imports as a skeleton pack. Demo exercises included. | Answer sheets, a notes app and guesswork |
Expand Down Expand Up @@ -741,8 +741,9 @@ extra) and *Predict the next activity*. Or start empty.
one click adds the box that reads the file, already set. Type to search
them all: "alpha" finds the α-algorithm, "boarding" the log; Enter adds the
first match.
- **Files flow into the canvas.** Drop a log, a PNML file, a .cpn or a
ts.txt on the canvas and it becomes its input box at that spot; an input
- **Files flow into the canvas.** Drop a log, an OCEL 2.0 object-centric log
(`.jsonocel`), a PNML file, a .cpn or a ts.txt on the canvas and it becomes
its input box at that spot; an input
box names its file ("INPUT · boarding.xes"). *Open log…* and *Log from
notation…* in the sidebar land as boxes on the current analysis (or a new
one named after the file). In a box's **Settings**, *Choose…* picks the
Expand Down
2 changes: 1 addition & 1 deletion openprocess/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
#: The one place the version number is written (pyproject.toml, the update
#: check and packaging/build.py read it from here). A release is tagged
#: "v" + this, e.g. v0.2.0.
__version__ = "0.9.5"
__version__ = "0.10.5"

__all__ = [
"Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
Expand Down
193 changes: 193 additions & 0 deletions openprocess/citation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""Citations: BibTeX for the app, and for the works each algorithm follows.

The reference list (:mod:`openprocess.references`) is written for people, one
line per work. :func:`entry` turns such a line into a BibTeX entry, well
enough for a bibliography: the authors before the title, the title between
asterisks, the venue after it, the year at the end, and the DOI or URL when
the reference has one. :func:`app_bibtex` cites the app itself, with its
version; ``CITATION.cff`` at the top of the repository says the same for
GitHub's *Cite this repository*.
"""

from __future__ import annotations

import datetime as _dt
import re
from dataclasses import dataclass, field

from . import references as _references

APP_TITLE = "OpenProcess: process mining, Petri nets and workflows in one open app"
APP_URL = "https://github.com/s4mstruthers/openprocess"
APP_AUTHORS = (("Struthers", "Sam"),)


def app_version() -> str:
from . import __version__
return __version__


# ---------------------------------------------------------------------------
# One entry
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class Entry:
"""A BibTeX entry: its kind (``article``, ``book``, …), key and fields, in order."""

kind: str
key: str
fields: dict[str, str] = field(default_factory=dict)

def bibtex(self) -> str:
width = max((len(name) for name in self.fields), default=0)
lines = [f"@{self.kind}{{{self.key},"]
for name, value in self.fields.items():
if value:
lines.append(f" {name.ljust(width)} = {{{_escape(value)}}},")
lines.append("}")
return "\n".join(lines)


def _escape(value: str) -> str:
return value.replace("&", r"\&").replace("%", r"\%").replace("#", r"\#")


_TITLE = re.compile(r"\*(.+?)\*")
_YEAR = re.compile(r"\b(19|20)\d{2}\b")
_VOLUME = re.compile(r"\s*(\d+)\((\d+)\):(\d+[–-]\d+)")
_PAGES = re.compile(r"pp\.\s*(\d+[–-]\d+)")
_PROCEEDINGS = ("LNCS", "LNBIP", "Proceedings", "Workshop", "Conference", "Demo Track", "CEUR", "Symposium",
"ICPM", "Models of Concurrency")


def entry(reference: _references.Reference) -> Entry:
"""The reference as a BibTeX entry (its kind guessed from the venue)."""
text = reference.citation.strip()
year_match = None
for year_match in _YEAR.finditer(text):
pass
year = year_match.group(0) if year_match else ""
link = reference.link or ""
doi, url = (link, "") if link.startswith("10.") else ("", link)
title_match = _TITLE.search(text)
if title_match is None: # a standard or a tool, named in one line
return Entry("misc", reference.key, {"title": text.rstrip("."), "year": year, "doi": doi, "url": url})
authors = text[:title_match.start()].strip().rstrip(".,").strip()
title = title_match.group(1).strip().rstrip(",")
rest = text[title_match.end():].strip().lstrip(".,").strip().rstrip(".")
rest = re.sub(r",?\s*" + re.escape(year) + r"\s*$", "", rest).strip().rstrip(",.") if year else rest
fields: dict[str, str] = {}
if any(ch.isdigit() for ch in authors) or " " not in authors:
fields["organization"] = authors # "ISO/IEC 15909-2:2011"
else:
fields["author"] = " and ".join(part.strip() for part in re.split(r",\s*|\s+and\s+", authors) if part.strip())
fields["title"] = title
lower = text.lower()
volume = _VOLUME.search(rest)
if "thesis" in lower:
kind = "phdthesis"
fields["school"] = re.sub(r"^phd thesis,?\s*", "", rest, flags=re.I)
elif "working paper" in lower or "technical report" in lower:
kind = "techreport"
fields["institution"] = rest
elif volume is not None:
kind = "article"
fields["journal"] = rest[:volume.start()].strip().rstrip(",")
fields["volume"], fields["number"], fields["pages"] = volume.group(1), volume.group(2), volume.group(3)
elif any(word in rest for word in _PROCEEDINGS):
kind = "inproceedings"
booktitle, publisher = rest, ""
if ". Springer" in rest or rest.endswith("Springer"):
booktitle, _, publisher = rest.rpartition(". ")
publisher = publisher.strip() or "Springer"
pages = _PAGES.search(booktitle)
if pages:
fields["pages"] = pages.group(1)
booktitle = _PAGES.sub("", booktitle).strip().rstrip(",")
fields["booktitle"] = booktitle
fields["publisher"] = publisher
elif "edition" in rest or rest.endswith("Press") or rest.startswith("Springer") or rest.startswith("Cambridge"):
kind = "book"
edition = re.search(r"(\d+)(st|nd|rd|th) edition", rest)
if edition:
fields["edition"] = edition.group(1)
rest = re.sub(r",?\s*\d+(st|nd|rd|th) edition\.?\s*", " ", rest).strip().rstrip(".,")
fields["publisher"] = rest
else:
kind = "misc"
fields["howpublished"] = rest
fields["year"] = year
fields["doi"] = doi
fields["url"] = url
return Entry(kind, reference.key, fields)


def entries_for_module(module: str) -> list[Entry]:
"""The works the code in ``module`` follows (see :func:`references.topics_for_module`)."""
seen, out = set(), []
for topic in _references.topics_for_module(module):
for key in topic.sources:
reference = _references.reference(key)
if reference is not None and key not in seen:
seen.add(key)
out.append(entry(reference))
return out


# ---------------------------------------------------------------------------
# The app itself
# ---------------------------------------------------------------------------
def app_entry(version: str | None = None, year: int | None = None) -> Entry:
version = version or app_version()
year = year or _dt.date.today().year
return Entry("software", "openprocess", {
"author": " and ".join(f"{last}, {first}" for last, first in APP_AUTHORS),
"title": APP_TITLE,
"version": version,
"year": str(year),
"url": APP_URL,
"note": "Every algorithm shows its code and the paper it follows; every analysis keeps a record "
"that runs again",
})


def app_bibtex(version: str | None = None, year: int | None = None) -> str:
return app_entry(version, year).bibtex()


def app_text(version: str | None = None, year: int | None = None) -> str:
"""The citation in one line, as a reference list would have it."""
version = version or app_version()
year = year or _dt.date.today().year
authors = ", ".join(f"{first} {last}" for last, first in APP_AUTHORS)
return f"{authors}. {APP_TITLE} (version {version}) [software], {year}. {APP_URL}"


def cff_text(version: str | None = None, released: _dt.date | None = None) -> str:
"""The contents of ``CITATION.cff`` (Citation File Format 1.2.0) for ``version``."""
version = version or app_version()
released = released or _dt.date.today()
authors = "\n".join(f" - family-names: {last}\n given-names: {first}" for last, first in APP_AUTHORS)
return (
"cff-version: 1.2.0\n"
"message: If you use OpenProcess in teaching or research, please cite it as below.\n"
"type: software\n"
f"title: {APP_TITLE}\n"
f"version: {version}\n"
f"date-released: {released.isoformat()}\n"
f"repository-code: {APP_URL}\n"
f"url: {APP_URL}\n"
"license: MIT\n"
"keywords:\n - process mining\n - Petri nets\n - workflow nets\n - conformance checking\n"
" - teaching\n - reproducible research\n"
f"authors:\n{authors}\n"
"abstract: >-\n"
" OpenProcess is a desktop app for process mining and Petri net modelling in which\n"
" every algorithm is a function whose code and source paper are one click away, and\n"
" every analysis keeps a record of its inputs, versions and seeds so that its numbers\n"
" can be checked again.\n"
)


__all__ = ["Entry", "entry", "entries_for_module", "app_entry", "app_bibtex", "app_text", "cff_text",
"APP_TITLE", "APP_URL"]
16 changes: 15 additions & 1 deletion openprocess/flow/boxes/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ...mining.processtree import ProcessTree, to_petri_net
from ...mining.transition_system import TransitionSystem, transition_system_from_log
from ..box import box
from ..types import Table
from ..types import OCDFG, OCEL, Table


@box(name="α-algorithm", group="Discover")
Expand Down Expand Up @@ -110,6 +110,20 @@ def regions_to_net(ts: TransitionSystem) -> PetriNet:
return result.net


@box(name="Object-centric map", group="Discover")
def object_centric_map(ocel: OCEL) -> OCDFG:
"""The object-centric directly-follows graph: every object followed
through its events, so each object type gets its own directly-follows
counts, drawn in its own colour on the shared activities. No case id is
invented, so nothing converges or diverges that did not."""
from ...mining.ocel import object_centric_dfg
graph = object_centric_dfg(ocel)
for object_type in graph.object_types:
flow.note(f"{object_type}: {len(graph.edges[object_type])} paths, "
f"starts at {', '.join(sorted(graph.starts[object_type])) or '–'}")
return graph


@box(name="Directly-follows graph", group="Discover")
def directly_follows(log: EventLog) -> DFG:
"""The process map: which activity directly follows which, how often."""
Expand Down
Loading
Loading