openprocess 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. cpnpy/__init__.py +46 -0
  2. openprocess/__init__.py +57 -0
  3. openprocess/analysis/__init__.py +0 -0
  4. openprocess/analysis/state_space.py +521 -0
  5. openprocess/analysis/state_space_process.py +251 -0
  6. openprocess/cli.py +742 -0
  7. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
  8. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
  9. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
  10. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
  11. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
  12. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
  13. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
  14. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
  15. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
  16. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
  17. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
  18. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
  19. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
  20. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
  21. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
  22. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
  23. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
  24. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
  25. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
  26. openprocess/exercises/pack.md +14 -0
  27. openprocess/flow/__init__.py +50 -0
  28. openprocess/flow/box.py +466 -0
  29. openprocess/flow/boxes/__init__.py +7 -0
  30. openprocess/flow/boxes/check.py +119 -0
  31. openprocess/flow/boxes/compare.py +16 -0
  32. openprocess/flow/boxes/cpn.py +53 -0
  33. openprocess/flow/boxes/discover.py +124 -0
  34. openprocess/flow/boxes/filter.py +80 -0
  35. openprocess/flow/boxes/input.py +124 -0
  36. openprocess/flow/boxes/output.py +52 -0
  37. openprocess/flow/boxes/predict.py +186 -0
  38. openprocess/flow/boxes/science.py +159 -0
  39. openprocess/flow/boxes/sweeps.py +18 -0
  40. openprocess/flow/convert.py +187 -0
  41. openprocess/flow/datasets.py +198 -0
  42. openprocess/flow/explain.py +115 -0
  43. openprocess/flow/library.py +222 -0
  44. openprocess/flow/record.py +385 -0
  45. openprocess/flow/runner.py +357 -0
  46. openprocess/flow/sweep.py +92 -0
  47. openprocess/flow/types.py +290 -0
  48. openprocess/flow/workflow.py +628 -0
  49. openprocess/gui/__init__.py +0 -0
  50. openprocess/gui/app.py +90 -0
  51. openprocess/gui/arc_editing.py +295 -0
  52. openprocess/gui/canvas.py +1414 -0
  53. openprocess/gui/flow/__init__.py +8 -0
  54. openprocess/gui/flow/canvas.py +854 -0
  55. openprocess/gui/flow/page.py +972 -0
  56. openprocess/gui/flow/templates.py +131 -0
  57. openprocess/gui/flow/viewers.py +665 -0
  58. openprocess/gui/items.py +1275 -0
  59. openprocess/gui/learn/answer_boxes.py +978 -0
  60. openprocess/gui/learn/concealment.py +91 -0
  61. openprocess/gui/learn/mode.py +1181 -0
  62. openprocess/gui/panning.py +241 -0
  63. openprocess/gui/resources/openprocess-icon.png +0 -0
  64. openprocess/gui/studio/__init__.py +1 -0
  65. openprocess/gui/studio/__main__.py +3 -0
  66. openprocess/gui/studio/app.py +4031 -0
  67. openprocess/gui/studio/charts.py +115 -0
  68. openprocess/gui/studio/compare_page.py +487 -0
  69. openprocess/gui/studio/cpn_page.py +1858 -0
  70. openprocess/gui/studio/definition_view.py +284 -0
  71. openprocess/gui/studio/derivation_view.py +421 -0
  72. openprocess/gui/studio/documents.py +152 -0
  73. openprocess/gui/studio/dotted_chart.py +1401 -0
  74. openprocess/gui/studio/file_dialogs.py +143 -0
  75. openprocess/gui/studio/filter_dialog.py +247 -0
  76. openprocess/gui/studio/graph_builders.py +176 -0
  77. openprocess/gui/studio/graph_view.py +682 -0
  78. openprocess/gui/studio/instances.py +413 -0
  79. openprocess/gui/studio/log_editor.py +675 -0
  80. openprocess/gui/studio/log_page.py +800 -0
  81. openprocess/gui/studio/markdown_view.py +127 -0
  82. openprocess/gui/studio/mathtext.py +260 -0
  83. openprocess/gui/studio/ml_highlighter.py +75 -0
  84. openprocess/gui/studio/model_page.py +760 -0
  85. openprocess/gui/studio/net_comparison.py +124 -0
  86. openprocess/gui/studio/notes_overlay.py +275 -0
  87. openprocess/gui/studio/petri_page.py +844 -0
  88. openprocess/gui/studio/regions_view.py +502 -0
  89. openprocess/gui/studio/sidebar.py +149 -0
  90. openprocess/gui/studio/style.py +503 -0
  91. openprocess/gui/studio/tool_icons.py +134 -0
  92. openprocess/gui/studio/updates.py +439 -0
  93. openprocess/gui/studio/widgets.py +899 -0
  94. openprocess/gui/studio/workers.py +60 -0
  95. openprocess/gui/studio/workspace.py +447 -0
  96. openprocess/gui/theme.py +394 -0
  97. openprocess/gui/tidy.py +86 -0
  98. openprocess/io/__init__.py +0 -0
  99. openprocess/io/cpn_reader.py +389 -0
  100. openprocess/io/cpn_writer.py +357 -0
  101. openprocess/learn/__init__.py +23 -0
  102. openprocess/learn/answers.py +188 -0
  103. openprocess/learn/checks.py +953 -0
  104. openprocess/learn/computed.py +1180 -0
  105. openprocess/learn/context.py +145 -0
  106. openprocess/learn/exam.py +169 -0
  107. openprocess/learn/exercise-packs.md +325 -0
  108. openprocess/learn/importer.py +216 -0
  109. openprocess/learn/notation.py +474 -0
  110. openprocess/learn/pack.py +511 -0
  111. openprocess/learn/sheet.py +296 -0
  112. openprocess/mining/__init__.py +73 -0
  113. openprocess/mining/analysis.py +689 -0
  114. openprocess/mining/columns.py +282 -0
  115. openprocess/mining/compare_nets.py +246 -0
  116. openprocess/mining/conformance/__init__.py +0 -0
  117. openprocess/mining/conformance/alignments.py +263 -0
  118. openprocess/mining/conformance/quality.py +145 -0
  119. openprocess/mining/conformance/token_replay.py +252 -0
  120. openprocess/mining/csv_import.py +222 -0
  121. openprocess/mining/definitions.py +584 -0
  122. openprocess/mining/dfg.py +187 -0
  123. openprocess/mining/discovery/__init__.py +0 -0
  124. openprocess/mining/discovery/alpha.py +168 -0
  125. openprocess/mining/discovery/heuristics.py +332 -0
  126. openprocess/mining/discovery/inductive.py +477 -0
  127. openprocess/mining/discovery/state_regions.py +62 -0
  128. openprocess/mining/filtering.py +237 -0
  129. openprocess/mining/footprint.py +183 -0
  130. openprocess/mining/invariants.py +191 -0
  131. openprocess/mining/layout.py +279 -0
  132. openprocess/mining/log.py +364 -0
  133. openprocess/mining/petrinet.py +354 -0
  134. openprocess/mining/playout.py +75 -0
  135. openprocess/mining/pm4py_bridge.py +82 -0
  136. openprocess/mining/pnml.py +223 -0
  137. openprocess/mining/processtree.py +216 -0
  138. openprocess/mining/regions.py +476 -0
  139. openprocess/mining/stats.py +160 -0
  140. openprocess/mining/structure.py +374 -0
  141. openprocess/mining/transition_system.py +409 -0
  142. openprocess/mining/xes.py +399 -0
  143. openprocess/ml/__init__.py +0 -0
  144. openprocess/ml/ast_nodes.py +332 -0
  145. openprocess/ml/builtins.py +364 -0
  146. openprocess/ml/colorsets.py +522 -0
  147. openprocess/ml/errors.py +60 -0
  148. openprocess/ml/evaluator.py +754 -0
  149. openprocess/ml/lexer.py +277 -0
  150. openprocess/ml/multiset.py +417 -0
  151. openprocess/ml/parser.py +737 -0
  152. openprocess/ml/values.py +319 -0
  153. openprocess/model/__init__.py +0 -0
  154. openprocess/model/declarations.py +617 -0
  155. openprocess/model/examples.py +98 -0
  156. openprocess/model/net.py +701 -0
  157. openprocess/model/plain.py +192 -0
  158. openprocess/references.py +280 -0
  159. openprocess/sim/__init__.py +0 -0
  160. openprocess/sim/binding.py +620 -0
  161. openprocess/sim/export.py +66 -0
  162. openprocess/sim/simulator.py +315 -0
  163. openprocess/teaching/__init__.py +4 -0
  164. openprocess/teaching/answers.py +4 -0
  165. openprocess/teaching/checks.py +5 -0
  166. openprocess/teaching/pack.py +4 -0
  167. openprocess/teaching/sheet.py +4 -0
  168. openprocess-0.7.0.dist-info/METADATA +927 -0
  169. openprocess-0.7.0.dist-info/RECORD +173 -0
  170. openprocess-0.7.0.dist-info/WHEEL +5 -0
  171. openprocess-0.7.0.dist-info/entry_points.txt +6 -0
  172. openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
  173. openprocess-0.7.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,145 @@
1
+ """What an exercise gives: its files, read once, and the student's variant.
2
+
3
+ A :class:`Context` is made for one exercise and handed to every check and
4
+ compute. It reads the log, the net and the transition system the first
5
+ time they are asked for, so checking ten answer boxes reads the log once.
6
+
7
+ Variants (``seed: student`` in the exercise or the pack, with
8
+ ``generate: net.pnml, cases: 20``): the exercise has no log file; its log is
9
+ played out from the net with a seed made from the student's name, so every
10
+ student gets a log of their own and the computes still check every answer
11
+ (see :mod:`.pack` for where the name is kept).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import zlib
17
+ from functools import cached_property
18
+ from pathlib import Path
19
+
20
+ from .pack import Exercise
21
+
22
+
23
+ class TaskError(ValueError):
24
+ """The answer block cannot be checked (a missing file, an unknown compute)."""
25
+
26
+
27
+ class Context:
28
+ """The exercise's files, read once and only when a check needs them."""
29
+
30
+ def __init__(self, exercise: Exercise, student: str | None = None) -> None:
31
+ self.exercise = exercise
32
+ self.files = exercise.files
33
+ #: The student's name, for variants (None: the pack has no variants).
34
+ self.student = student if student is not None else exercise.student()
35
+ self._cache: dict = {}
36
+
37
+ # -- files ------------------------------------------------------------------------
38
+ def path(self, name: str | None, default: Path | None, what: str) -> Path:
39
+ if name:
40
+ path = self.files.file(name)
41
+ if path is None:
42
+ raise TaskError(f"the exercise has no file “{name}”")
43
+ return path
44
+ if default is None:
45
+ raise TaskError(f"the exercise has no {what}")
46
+ return default
47
+
48
+ @property
49
+ def seed(self) -> int | None:
50
+ """The seed of the student's variant (None: no variants)."""
51
+ setting = self.exercise.setting("seed")
52
+ if setting is None:
53
+ return None
54
+ if setting.strip().lower() in ("student", "name", "per student"):
55
+ return zlib.crc32((self.student or "").strip().casefold().encode("utf-8"))
56
+ try:
57
+ return int(setting)
58
+ except ValueError:
59
+ raise TaskError(f"“seed: {setting}” is neither a number nor “student”") from None
60
+
61
+ def generated_log(self):
62
+ """The log played out from ``generate: net.pnml, cases: 20`` (an EventLog),
63
+ or None when the exercise does not generate one."""
64
+ setting = self.exercise.setting("generate")
65
+ if not setting:
66
+ return None
67
+ if "generated" in self._cache:
68
+ return self._cache["generated"]
69
+ from ..mining.playout import play_out
70
+ from .exam import parse_generate
71
+ net_name, cases, max_length = parse_generate(setting)
72
+ net = self.net(net_name)
73
+ from ..mining.analysis import check_workflow_net
74
+ from ..mining.petrinet import Marking
75
+ if not net.initial_marking:
76
+ workflow = check_workflow_net(net)
77
+ if workflow.is_workflow_net:
78
+ net = net.copy()
79
+ net.initial_marking = Marking({workflow.source: 1})
80
+ net.final_marking = Marking({workflow.sink: 1})
81
+ seed = self.seed if self.seed is not None else 0
82
+ log = play_out(net, traces=cases, max_length=max_length, seed=seed,
83
+ name=f"Generated log (seed {seed})").log
84
+ self._cache["generated"] = log
85
+ return log
86
+
87
+ def event_log(self, name: str | None = None):
88
+ """The exercise's log as an :class:`EventLog` (with timestamps when the
89
+ file has them)."""
90
+ key = ("event_log", name)
91
+ if key in self._cache:
92
+ return self._cache[key]
93
+ from ..mining.csv_import import guess_mapping, read_csv, sniff
94
+ from ..mining.log import EventLog, parse_simple_log
95
+ from ..mining.xes import read_xes
96
+ if not name and self.files.log is None:
97
+ generated = self.generated_log()
98
+ if generated is not None:
99
+ self._cache[key] = generated
100
+ return generated
101
+ path = self.path(name, self.files.log, "log (log.txt, log.xes or log.csv)")
102
+ lower = path.name.lower()
103
+ if lower.endswith(".txt"):
104
+ log = EventLog.from_simple_log(
105
+ parse_simple_log(path.read_text(encoding="utf-8", errors="replace")), "L")
106
+ elif lower.endswith(".csv"):
107
+ log = read_csv(str(path), guess_mapping(sniff(str(path))[1]))
108
+ else:
109
+ log = read_xes(str(path))
110
+ self._cache[key] = log
111
+ return log
112
+
113
+ def simple_log(self, name: str | None = None):
114
+ """The log as a multiset of activity sequences."""
115
+ key = ("simple_log", name)
116
+ if key not in self._cache:
117
+ self._cache[key] = self.event_log(name).simple_log()
118
+ return self._cache[key]
119
+
120
+ def net(self, name: str | None = None):
121
+ key = ("net", name)
122
+ if key not in self._cache:
123
+ from ..mining.pnml import read_pnml
124
+ self._cache[key] = read_pnml(str(self.path(name, self.files.net, "net (net.pnml)")))
125
+ return self._cache[key]
126
+
127
+ def ts(self, name: str | None = None):
128
+ key = ("ts", name)
129
+ if key not in self._cache:
130
+ from ..mining.transition_system import parse_transition_system
131
+ path = self.path(name, self.files.ts, "transition system (ts.txt)")
132
+ self._cache[key] = parse_transition_system(
133
+ path.read_text(encoding="utf-8", errors="replace"))
134
+ return self._cache[key]
135
+
136
+ @cached_property
137
+ def alpha(self):
138
+ from ..mining.discovery.alpha import alpha_miner
139
+ return alpha_miner(self.simple_log())
140
+
141
+ @cached_property
142
+ def library(self):
143
+ """The boxes a workflow in this exercise may use (see :mod:`openprocess.flow`)."""
144
+ from ..flow.library import library_for
145
+ return library_for(self.exercise.folder)
@@ -0,0 +1,169 @@
1
+ """Exams, points and variants.
2
+
3
+ * **Points**: every answer block has ``points:`` (1 when it says nothing); an
4
+ exercise's points are its blocks' (or ``points:`` in its front matter), and
5
+ :func:`marks` turns a pack into a marks table (``openprocess exercises marks``).
6
+ * **Exam mode**: ``exam: yes`` in ``pack.md``'s front matter. The app then
7
+ shows no hints, no *Show answer* and reveals no hidden result; ``time: 120``
8
+ (minutes) runs a clock from the moment the pack is first opened
9
+ (:class:`ExamState`, kept in ``my exam.json``), and ``deadline: 2026-11-01
10
+ 12:00`` closes the answers at that moment. After the time is up the
11
+ answers stay as they are and can still be read.
12
+ * **Variants**: ``seed: student`` with ``generate: net.pnml, cases: 20``
13
+ (see :meth:`openprocess.learn.context.Context.generated_log`).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import csv
19
+ import io
20
+ import json
21
+ import os
22
+ import re
23
+ from dataclasses import dataclass
24
+ from datetime import datetime, timedelta
25
+ from pathlib import Path
26
+
27
+ from .pack import MY_EXAM, Pack
28
+
29
+
30
+ def parse_generate(setting: str) -> tuple[str, int, int]:
31
+ """``generate: net.pnml, cases: 20, length: 50`` → ``("net.pnml", 20, 50)``."""
32
+ parts = [p.strip() for p in setting.split(",") if p.strip()]
33
+ if not parts:
34
+ raise ValueError("generate: needs the net to play out")
35
+ net, cases, length = parts[0], 20, 200
36
+ for part in parts[1:]:
37
+ match = re.match(r"^(cases|traces|length|max length)\s*[:=]?\s*(\d+)$", part, re.I)
38
+ if not match:
39
+ raise ValueError(f"cannot read “{part}” in generate: (use cases: 20, length: 50)")
40
+ if match.group(1).lower() in ("cases", "traces"):
41
+ cases = int(match.group(2))
42
+ else:
43
+ length = int(match.group(2))
44
+ return net, cases, length
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # The clock of an exam
49
+ # ---------------------------------------------------------------------------
50
+ @dataclass
51
+ class ExamState:
52
+ """When the exam was started and when it ends."""
53
+
54
+ pack: Pack
55
+ started: datetime | None = None
56
+
57
+ @property
58
+ def path(self) -> Path:
59
+ return self.pack.root / MY_EXAM
60
+
61
+ @classmethod
62
+ def load(cls, pack: Pack) -> "ExamState":
63
+ state = cls(pack)
64
+ try:
65
+ data = json.loads(state.path.read_text(encoding="utf-8"))
66
+ state.started = datetime.fromisoformat(data["started"])
67
+ except (OSError, ValueError, KeyError, TypeError):
68
+ state.started = None
69
+ return state
70
+
71
+ def start(self, now: datetime | None = None) -> None:
72
+ """Note the start (only the first time)."""
73
+ if self.started is not None:
74
+ return
75
+ self.started = (now or datetime.now()).replace(microsecond=0)
76
+ text = json.dumps({"started": self.started.isoformat(), "pack": self.pack.title}, indent=2)
77
+ temporary = self.path.with_name("." + MY_EXAM + ".tmp")
78
+ temporary.write_text(text, encoding="utf-8")
79
+ os.replace(temporary, self.path)
80
+
81
+ @property
82
+ def minutes(self) -> int | None:
83
+ text = (self.pack.settings.get("time") or "").strip().lower()
84
+ match = re.match(r"^(\d+)\s*(min|minutes|m|h|hours|hour)?$", text)
85
+ if not match:
86
+ return None
87
+ value = int(match.group(1))
88
+ return value * 60 if (match.group(2) or "").startswith("h") else value
89
+
90
+ @property
91
+ def deadline(self) -> datetime | None:
92
+ """The earlier of ``deadline:`` and start + ``time:``."""
93
+ candidates = []
94
+ text = (self.pack.settings.get("deadline") or "").strip()
95
+ if text:
96
+ try:
97
+ candidates.append(datetime.fromisoformat(text.replace("/", "-")))
98
+ except ValueError:
99
+ pass
100
+ if self.started is not None and self.minutes is not None:
101
+ candidates.append(self.started + timedelta(minutes=self.minutes))
102
+ return min(candidates) if candidates else None
103
+
104
+ def remaining(self, now: datetime | None = None) -> timedelta | None:
105
+ deadline = self.deadline
106
+ if deadline is None:
107
+ return None
108
+ return deadline - (now or datetime.now())
109
+
110
+ def closed(self, now: datetime | None = None) -> bool:
111
+ remaining = self.remaining(now)
112
+ return remaining is not None and remaining.total_seconds() <= 0
113
+
114
+ def clock_text(self, now: datetime | None = None) -> str:
115
+ remaining = self.remaining(now)
116
+ if remaining is None:
117
+ return "Exam"
118
+ if remaining.total_seconds() <= 0:
119
+ return "Time is up"
120
+ total = int(remaining.total_seconds())
121
+ hours, rest = divmod(total, 3600)
122
+ minutes, seconds = divmod(rest, 60)
123
+ return f"{hours}:{minutes:02d}:{seconds:02d} left" if hours else f"{minutes}:{seconds:02d} left"
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Marks
128
+ # ---------------------------------------------------------------------------
129
+ def marks(pack: Pack) -> list[dict]:
130
+ """One row per exercise: its points, what the answers earn, and per block."""
131
+ rows = []
132
+ for exercise in pack.exercises:
133
+ progress = exercise.load_progress()
134
+ summary = exercise.summary(progress)
135
+ row = {"exercise": exercise.title, "chapter": pack.chapter(exercise),
136
+ "points": summary.points, "earned": round(summary.earned, 2),
137
+ "answered": summary.tried, "of": summary.total, "blocks": {}}
138
+ for task in exercise.sheet.tasks:
139
+ entry = progress.get(task.id, {})
140
+ status = entry.get("status", "")
141
+ share = {"correct": 1.0, "done": 1.0}.get(status, float(entry.get("share", 0) or 0)
142
+ if status == "partial" else 0.0)
143
+ row["blocks"][task.id] = {"status": status, "points": task.points,
144
+ "earned": round(task.points * share, 2)}
145
+ rows.append(row)
146
+ return rows
147
+
148
+
149
+ def marks_csv(pack: Pack) -> str:
150
+ """The marks as CSV: exercise, chapter, points, earned, answered, of; then a
151
+ column per answer block."""
152
+ rows = marks(pack)
153
+ out = io.StringIO()
154
+ writer = csv.writer(out)
155
+ writer.writerow(["student", "exercise", "chapter", "points", "earned", "answered", "of",
156
+ "block", "block points", "block earned", "status"])
157
+ student = pack.student() or ""
158
+ for row in rows:
159
+ if not row["blocks"]:
160
+ writer.writerow([student, row["exercise"], row["chapter"], row["points"], row["earned"],
161
+ row["answered"], row["of"], "", "", "", ""])
162
+ for block_id, block in row["blocks"].items():
163
+ writer.writerow([student, row["exercise"], row["chapter"], row["points"], row["earned"],
164
+ row["answered"], row["of"], block_id, block["points"],
165
+ block["earned"], block["status"]])
166
+ total = sum(r["points"] for r in rows)
167
+ earned = sum(r["earned"] for r in rows)
168
+ writer.writerow([student, "TOTAL", "", total, round(earned, 2), "", "", "", "", "", ""])
169
+ return out.getvalue()
@@ -0,0 +1,325 @@
1
+ # Writing exercise packs
2
+
3
+ An exercise pack is a folder of worksheets. Students open it in OpenProcess
4
+ (*Learn ▸ Open Exercise Pack…*, or click an exercise in the sidebar), answer
5
+ in the boxes on each sheet, and press **Check**. Most answers are checked by
6
+ the app — often against answers it works out itself from the log, net or
7
+ transition system you give, so you do not have to. A pack can also be an
8
+ **exam**: a clock, points, no hints, and marks you export at the end.
9
+
10
+ ## Making a pack, step by step
11
+
12
+ 1. **Make the folder.** One folder for the pack, with a `pack.md` (its title
13
+ and a short introduction), and one subfolder per exercise. Name them so they
14
+ sort in order: `1 Dotted chart`, `2 Process modelling`, …
15
+ 2. **Add what each exercise gives.** A log as `log.txt` in the course's notation
16
+ (or `log.xes` / `log.csv`), a net as `net.pnml`, a transition system as
17
+ `ts.txt`. To make a net, draw it in OpenProcess (*File ▸ New Petri Net*), give it
18
+ its initial (and final) marking, and save it into the exercise folder as
19
+ `net.pnml`. Pictures (`.png`) go in the folder too.
20
+ 3. **Write `question.md`.** The question as students would read it on paper,
21
+ with an `answer` block wherever they should answer (see *Choosing a box*
22
+ below). Give every block a `solution` that explains the answer, or a grading
23
+ scheme for open questions.
24
+ 4. **Check it.** Run `openprocess exercises check "My pack" --answers` (see
25
+ *Checking a pack*): it finds mistakes in the blocks and prints every answer
26
+ the app works out, so you can compare them with your own.
27
+ 5. **Try it.** Open the pack in OpenProcess, answer a few boxes right and wrong, and
28
+ look at what Check says. Then delete the `my answers.json`, `my answer.pnml`,
29
+ `my workflow.cpnflow` and `my notes.md` files your try left behind.
30
+ 6. **Share it.** Zip the folder or put it in a shared drive. Students open it
31
+ with *Learn ▸ Open Exercise Pack…*.
32
+
33
+ Starting from a **past exam**? *Learn ▸ Make a Pack from an Exam…* (or
34
+ `openprocess exercises import exam.txt "Exam 2025"`) turns its text into a skeleton
35
+ pack: see *Turning a past exam into a pack* below.
36
+
37
+ ### Choosing a box
38
+
39
+ - One right answer from a list (true/false, "optimal / sub-optimal / not an
40
+ alignment", "bounded with k = 1"): `choice`, or `yesno` for yes/no.
41
+ - A property of the given net, log or transition system: `yesno` or `set`
42
+ with `compute:`, so the app works the answer out and it cannot be wrong.
43
+ - A set, a set of sets, pairs ($Y_L$, regions, dead transitions): `set`.
44
+ - A figure (fitness, costs, counts): `number`, with `tolerance:` when it is
45
+ rounded (`tolerance: 0.00005` for four decimals).
46
+ - The footprint of the given log: `footprint`.
47
+ - "Give a firing sequence that…": `trace`.
48
+ - A marking, or the set of reachable or terminal markings: `marking`,
49
+ `markings`.
50
+ - "Write the net as $(P, T, F, m_0)$": `tuple`.
51
+ - The incidence matrix, $M$ or $M'$ of region theory: `matrix`.
52
+ - "Draw the reachability graph": `ts` (typed as `s0 -a-> s1` lines).
53
+ - The Inductive Miner's steps: `cut`, `log` (a sublog), `tree`.
54
+ - Token replay by hand: `replay` (p, c, m, r per trace); an alignment to
55
+ construct: `alignment`; models to put in order: `ranking`.
56
+ - "Before running it, what will the algorithm give?": `predict`, checked
57
+ against a box of the workflow library run on the exercise's log.
58
+ - "Build an analysis": `workflow`, built on the canvas beside the sheet.
59
+ - "Draw a net" or "correct this net": `net`, with `sound: yes` and, when the
60
+ behaviour has one right answer, `answer:`. To have students correct a given
61
+ net, give it as a file and `start:` from it.
62
+ - Explanations, derivations, drawings other than nets: `open`, with the
63
+ grading scheme as `solution`. Students write in the box (or in Notes) and
64
+ compare.
65
+
66
+ ### Pictures
67
+
68
+ `![Model (a)](model-a.png)` shows a picture from the exercise folder.
69
+ Pictures wider than the worksheet are scaled to fit. A picture in a
70
+ `solution` is only shown after *Show answer*, which is the place for model
71
+ answers drawn as pictures.
72
+
73
+ ## The folder
74
+
75
+ ```
76
+ Week 3 — Discovery/
77
+ pack.md title and introduction (optional)
78
+ 1 Footprints/ chapters are just subfolders (optional)
79
+ Exercise 1.1 First log/
80
+ question.md the worksheet
81
+ log.txt files the exercise gives
82
+ 2 Alpha/
83
+ Exercise 2.1 …/
84
+ ```
85
+
86
+ - Any folder with a `question.md` is an exercise. Exercises and chapters are
87
+ listed in name order, with numbers sorted as numbers.
88
+ - `pack.md` starts with `# Title`; the rest is shown on the pack's overview.
89
+ It may start with *front matter* (`---` lines) for the whole pack: `exam`,
90
+ `time`, `deadline`, `seed`, `generate` (see *Exams* and *Variants*).
91
+ - An exercise can give **one** of each: `log.txt` (the course's notation,
92
+ `[<a,b,c>^3, <a,c>]`), `log.xes` or `log.csv`; `net.pnml`; `ts.txt`
93
+ (`s0 -a-> s1`, one per line or comma-separated, plus `initial: s0`). They are
94
+ opened beside the worksheet. Other files can be referred to by name
95
+ (`of: m2.pnml`, `over: m1.pnml, m2.pnml`).
96
+ - Students' work is saved next to the question: `my answers.json`,
97
+ `my answer.pnml` for a drawn net, `my workflow.cpnflow` for a built
98
+ workflow and `my notes.md` for their scratch notes. The pack's folder gets
99
+ `my name.txt` (variants) and `my exam.json` (when an exam was started).
100
+ Delete them to reset; leave them out when you share the pack.
101
+
102
+ ## The worksheet
103
+
104
+ `question.md` is ordinary Markdown, with maths between `$…$` (inline) or
105
+ `$$…$$` (display). Its first `# heading` is the exercise's title. Wherever
106
+ students should answer, put an **answer block**:
107
+
108
+ ````
109
+ **a.** Give the start activities $T_I$ of $L$.
110
+
111
+ ```answer
112
+ type: set
113
+ compute: alpha.T_I
114
+ hint: Which activities does a trace begin with?
115
+ ```
116
+ ````
117
+
118
+ The sheet is shown top to bottom with each block as an answer box, so it reads
119
+ like the paper version. Inside a block:
120
+
121
+ - `key: value` lines;
122
+ - lines starting with spaces continue the value above (for a long `solution`);
123
+ - a comment starts with at least two spaces and `#` (one space before `#` is
124
+ kept, since `a # b` is the α-algorithm's choice relation);
125
+ - `- [x] …` / `- [ ] …` lines are the options of a `choice`;
126
+ - `- …` lines are more accepted answers of a `text` question.
127
+
128
+ Every block can have:
129
+
130
+ | Key | Meaning |
131
+ |---|---|
132
+ | `type` | The kind of box (below). Required, except that a block with options is a `choice`. |
133
+ | `id` | A name for the answer in `my answers.json`. Default `q1`, `q2`, … by position: give ids if you will reorder questions after students started. |
134
+ | `points` | What a right answer earns (1 when left out). A partly right answer earns a part: 2 of 4 items, 3 of 4 parts of a tuple, 10 of 12 cells. |
135
+ | `hint` | Shown when the student asks for a hint (never in an exam). |
136
+ | `solution` | The model answer, shown on *Show answer* (Markdown; never in an exam). Without it, the right answer the app knows is shown. |
137
+
138
+ A sheet may start with front matter of its own (`points: 10` for the whole
139
+ exercise, `seed`, `generate`).
140
+
141
+ ## Types of answer box
142
+
143
+ | `type` | The student… | Checked against |
144
+ |---|---|---|
145
+ | `yesno` | picks Yes or No | `answer: yes` / `no`, or `compute:` a property |
146
+ | `choice` | picks one option (or several, when several are marked `[x]`) | the `[x]` options |
147
+ | `set` | types a set: `{a, b}`, sets of sets `{s1, s3}, {s2, s3}`, or pairs `({a}, {b, d}), …` | `answer:` in the same notation, or `compute:` |
148
+ | `number` | types a number (`0.75`, `3/4`, `75%`) | `answer:` (with `tolerance:`), or `compute:` |
149
+ | `text` | types a short answer | `answer:` and any `- alternative` lines (case and spaces ignored), or a regular expression `pattern:` |
150
+ | `footprint` | fills in a matrix of → ← ‖ # | the footprint of the exercise's log, or `of: some.pnml` for a net's; `pairs: (a, b), (b, c)` to ask about some cells only |
151
+ | `trace` | types a firing sequence `register, send letter` | firable in `net.pnml` (or `net: other.pnml`) and ending as `ends:` says; `unseen: yes` for a trace the log never shows |
152
+ | `net` | draws a Petri net in the editor beside the sheet | `answer:`, `sound:`, `fits:` (any combination); `exact: yes` for the same net, not just the same behaviour |
153
+ | `marking` | types a marking `[p1, p4^2]` (also `p1 + 2p4`) | `answer:` or `compute:` (`fire(a, [p1])`, `net.m0`, `state equation(⟨a, b⟩)`) |
154
+ | `markings` | types a set of markings `[p1], [p2, p3]` | `answer:` or `compute:` (`reachable`, `terminal`) |
155
+ | `tuple` | fills in $P$, $T$, $F$ (as pairs) and $m_0$ | the exercise's net (or `of:`), part by part |
156
+ | `ts` | types a transition system, `s0 -a-> s1` per line, `initial: s0` | `answer:` or `compute: reachability graph`, the same up to the names of states |
157
+ | `matrix` | fills in a grid with the rows and columns given | `answer:` (typed as rows with headers) or `compute:` (`incidence`, `language.M`, `language.M'`); `rows:` and `columns:` fix the headers |
158
+ | `cut` | types a cut `→ {a} {b, c, e} {d}` | `answer:` or `compute: im.cut` (`im.cut(2)` for the second group's sublog) |
159
+ | `log` | types a log `[<b,c>^3, <e>]` | `answer:` or `compute: im.split(2)` |
160
+ | `tree` | types a process tree `→(a, ×(∧(b, c), e), d)` | `answer:` or `compute: im.tree`; children of × and ∧ in any order |
161
+ | `replay` | fills in produced, consumed, missing, remaining per trace | `compute: replay` on the exercise's net (one trace with `trace:`) |
162
+ | `alignment` | types two rows of moves, `≫` for no move | the net: `answer: optimal` (default), `sub-optimal` or `not an alignment`, for the trace in `trace:` |
163
+ | `ranking` | puts the models in `over:` in order, best first | `by:` a figure (`fitness` by default; `order: low` when lower is better) |
164
+ | `predict` | answers as a set, number, yes/no or text (`as:`) | `box:` a box of the workflow library run on the exercise's log, then `value:` a part of its result (`net.transitions`) |
165
+ | `workflow` | builds a workflow in the Workflow tab | `needs:` boxes it must have; `result:` a box's value (`Check fit.metrics.fitness`) against `answer:` / `compute:` |
166
+ | `open` | writes freely | nothing: the student compares with `solution` and says how it went |
167
+
168
+ Answers are read leniently: case, spaces, `$…$`, `\{ \}` and LaTeX subscripts
169
+ (`s_1`, `s_{1}`) do not matter, a single set may be written with or without
170
+ its braces, and the typed notations show how they are read as the student
171
+ types. Operators may be written as words (`seq`, `xor`, `and`, `loop`) and
172
+ `≫` as `>>`.
173
+
174
+ ### Computed answers
175
+
176
+ `compute:` works the answer out from the exercise's own files, so it stays
177
+ right if you change the log or the net. `openprocess exercises computes` lists them
178
+ all with what each works out; the main ones:
179
+
180
+ | Of the log | Of the net (`net.pnml`, or `of: file.pnml`) | Of the transition system (`ts.txt`) |
181
+ |---|---|---|
182
+ | `activities`, `start activities`, `end activities`, `variants`, `cases`, `events` | `wf-net`, `sound`, `option to complete`, `proper completion`, `no dead transitions`, `dead transitions`, `dead(t)` | `regions`, `minimal regions` |
183
+ | `alpha.T_L`, `alpha.T_I`, `alpha.T_O`, `alpha.X_L`, `alpha.Y_L`, `alpha.P_L`, `alpha.place(({a}, {b}))` | `bounded`, `bound`, `bound(p)`, `safe`, `live`, `deadlock-free`, `deadlocks`, `reversible`, `terminating` | `ger(e)`, `pre-regions(e)`, `post-regions(e)` (minimal) |
184
+ | `dfg`, `prefixes`, `language.M`, `language.M'` | `free-choice`, `well-structured`, `s-coverable` | `region({s1, s3})` (yes/no) |
185
+ | `im.cut`, `im.cut(2)`, `im.split(2)`, `im.tree`, `im.dfg(2)` | `net.P`, `net.T`, `net.F`, `net.m0`, `pre(t)`, `post(t)`, `enabled([p1])`, `fire(t, [p1])` | `elementary`, `state separation`, `forward closure` |
186
+ | `fitness`, `replay`, `alignment fitness(⟨a, b⟩)`, `alignment cost(⟨a, b⟩)` | `reachable`, `terminal`, `reachability graph`, `incidence`, `parikh(⟨a, b⟩)`, `state equation(⟨a, b⟩)`, `synchronous product(⟨a, b⟩)` | |
187
+ | `batches(a)`, `weekends`, `timeout(14 days)`, `arrival rate` (logs with timestamps) | | |
188
+
189
+ **Of a box or a workflow:** `box(alpha_miner).net.transitions` runs a box of
190
+ the workflow library (`openprocess boxes` lists them) on the exercise's log or net
191
+ and follows the path into its result; `settings: {"noise": 0.2}` sets the
192
+ box's settings. `workflow(analysis.cpnflow).Check fit.metrics.fitness` runs a
193
+ workflow file given with the exercise and reads one box's result by its title.
194
+
195
+ ### Nets
196
+
197
+ ```answer
198
+ type: net
199
+ start: net.pnml # start from this net (default: the exercise's net.pnml, else empty)
200
+ answer: answer.pnml # same complete traces as this net (or: alpha, inductive)
201
+ sound: yes # must be a sound WF-net
202
+ fits: log # must replay every trace of the log
203
+ ```
204
+
205
+ `answer:` compares *behaviour*, not drawings: the student's net is right when it
206
+ allows exactly the same complete traces (silent steps ignored, transitions
207
+ matched by label). When it is not, the student sees the shortest traces that
208
+ differ and can replay them in the token game. `answer: alpha` compares with the
209
+ net the α-algorithm discovers from the exercise's log; `exact: yes` asks for
210
+ the same net (places and arcs), not just the same behaviour.
211
+
212
+ ### Traces
213
+
214
+ `ends:` is one of `any` (just firable), `final` (ends in the final marking),
215
+ `deadlock` (nothing enabled, case not completed), `stuck` (the final marking can
216
+ no longer be reached) or `improper` (a token in the sink with others left
217
+ behind).
218
+
219
+ ### Workflows
220
+
221
+ ```answer
222
+ type: workflow
223
+ needs: inductive_miner, check_fit # boxes the workflow must contain (ids from openprocess boxes)
224
+ result: Check fit.metrics.fitness # a box's result, by the box's title
225
+ answer: 1
226
+ tolerance: 0.01
227
+ ```
228
+
229
+ The Workflow tab beside the sheet starts with the exercise's log in a box (or
230
+ from `start: given.cpnflow`); the student adds boxes and wires them, and the
231
+ workflow is saved as `my workflow.cpnflow`. Check runs it and looks for the
232
+ boxes in `needs:`, then compares the result named in `result:`.
233
+
234
+ ## Points, exams and marks
235
+
236
+ Every block is worth its `points:` (1 by default), an exercise the sum of
237
+ its blocks (or `points:` in its front matter), and a partly right answer
238
+ earns a part. The overview and the foot of every sheet show the points so
239
+ far. `openprocess exercises marks "My pack"` prints them per exercise (`--blocks`
240
+ per answer box, `--csv` for a spreadsheet), and *⋯ ▸ Export Marks…* in the
241
+ app writes the same CSV.
242
+
243
+ An **exam** is a pack whose `pack.md` starts with
244
+
245
+ ```
246
+ ---
247
+ exam: yes
248
+ time: 120 # minutes from the moment the pack is first opened
249
+ deadline: 2026-11-01 12:00 # or a fixed moment (the earlier of the two counts)
250
+ ---
251
+ ```
252
+
253
+ In an exam there are no hints and no *Show answer*, nothing in the materials
254
+ can be revealed, Check only says whether an answer is right, and the top bar
255
+ shows the clock. When the time is up the answers stay as they are and can
256
+ still be read, but not changed. The start is kept in `my exam.json` in the
257
+ pack's folder.
258
+
259
+ ## Variants
260
+
261
+ With `seed: student` in `pack.md` (or in an exercise's front matter) and
262
+ `generate: net.pnml, cases: 20, length: 50` in the exercise, the exercise has
263
+ no log file: its log is played out from the net with a seed made from the
264
+ student's name, which the app asks for once (kept as `my name.txt`). Every
265
+ student gets a log of their own, and every `compute:` still checks every
266
+ answer. `seed: 7` gives one fixed log instead.
267
+
268
+ ## Hiding results
269
+
270
+ While students work on an exercise, everything that would give answers away —
271
+ soundness and the other analysis results, footprints, discovered models,
272
+ conformance figures, regions — is hidden in the materials beside the sheet,
273
+ with a *Reveal* button on each (none in an exam).
274
+
275
+ ## Checking a pack
276
+
277
+ Before sharing a pack, check it from a terminal:
278
+
279
+ ```
280
+ openprocess exercises check "Week 3 — Discovery"
281
+ ```
282
+
283
+ It lists every exercise and answer box, works out every computed answer, and
284
+ reports mistakes (an unknown key, a missing file, a compute it cannot do) with
285
+ their line numbers. Add `--answers` to print the right answers.
286
+ `openprocess exercises computes` lists every compute with what it works out.
287
+
288
+ ## Turning a past exam into a pack
289
+
290
+ *Learn ▸ Make a Pack from an Exam…* (or `openprocess exercises import exam.txt
291
+ "Exam 2025"`) reads the exam's text — questions numbered `1.`, `2.`, … with
292
+ parts `a)`, `b)`, … and points in brackets — and writes a skeleton pack: one
293
+ exercise per question, an answer block per part whose type is guessed from
294
+ the wording ("is the net sound?" → `yesno` with `compute: sound`; "give a
295
+ firing sequence that ends in a deadlock" → `trace`; "draw" → `net`), and the
296
+ points carried over. Every `TODO` in the written `question.md` files is
297
+ yours to finish; `openprocess exercises check` then lists what is still missing.
298
+
299
+ - **Recreate given nets and logs as files** rather than pictures, so students
300
+ can play the token game and the app can compute the answers. Check that the
301
+ computed answers agree with the grading scheme (`--answers`): when they do
302
+ not, look at the net again — a misread arc is the usual cause, but grading
303
+ schemes have slips too.
304
+ - **Statements to judge** ("the model is live", "transition a is dead") become
305
+ `yesno` blocks with `compute:`. A run of questions with the same answer for
306
+ each transition can become one `set` block (`compute: dead transitions`).
307
+ - **Multiple-choice questions** keep their options. Where the exam accepted
308
+ two answers, mark the best one and say in the `solution` that the other was
309
+ accepted too (or make it `open` when both are equally right).
310
+ - **Calculations** become `number` blocks for the result, and a `replay`
311
+ block for the intermediate figures (produced, consumed, missing and
312
+ remaining tokens), so a student finds where they went wrong.
313
+ - **Modelling questions** become `net` blocks with `sound: yes`, the grading
314
+ scheme as `solution` and the model answer as a picture in it.
315
+ - **Questions about material that is not in the pack** (a course dataset) can
316
+ stay, with a sentence saying so and the figures they rely on.
317
+
318
+ ## Older exercises
319
+
320
+ An exercise without answer blocks still works. A sheet written in lettered
321
+ parts (`a.`, `b.`, … at the start of a paragraph) gets a box under each part:
322
+ the net editor for the part that asks to draw or change a net, a text box for
323
+ the others, each with the same part of `answer.md` (`**a.** …`) as its model
324
+ answer. Otherwise, with an `answer.pnml` the student draws a net that is
325
+ compared with it, and with only an `answer.md` that file is the worked answer.