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.
- cpnpy/__init__.py +46 -0
- openprocess/__init__.py +57 -0
- openprocess/analysis/__init__.py +0 -0
- openprocess/analysis/state_space.py +521 -0
- openprocess/analysis/state_space_process.py +251 -0
- openprocess/cli.py +742 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
- openprocess/exercises/pack.md +14 -0
- openprocess/flow/__init__.py +50 -0
- openprocess/flow/box.py +466 -0
- openprocess/flow/boxes/__init__.py +7 -0
- openprocess/flow/boxes/check.py +119 -0
- openprocess/flow/boxes/compare.py +16 -0
- openprocess/flow/boxes/cpn.py +53 -0
- openprocess/flow/boxes/discover.py +124 -0
- openprocess/flow/boxes/filter.py +80 -0
- openprocess/flow/boxes/input.py +124 -0
- openprocess/flow/boxes/output.py +52 -0
- openprocess/flow/boxes/predict.py +186 -0
- openprocess/flow/boxes/science.py +159 -0
- openprocess/flow/boxes/sweeps.py +18 -0
- openprocess/flow/convert.py +187 -0
- openprocess/flow/datasets.py +198 -0
- openprocess/flow/explain.py +115 -0
- openprocess/flow/library.py +222 -0
- openprocess/flow/record.py +385 -0
- openprocess/flow/runner.py +357 -0
- openprocess/flow/sweep.py +92 -0
- openprocess/flow/types.py +290 -0
- openprocess/flow/workflow.py +628 -0
- openprocess/gui/__init__.py +0 -0
- openprocess/gui/app.py +90 -0
- openprocess/gui/arc_editing.py +295 -0
- openprocess/gui/canvas.py +1414 -0
- openprocess/gui/flow/__init__.py +8 -0
- openprocess/gui/flow/canvas.py +854 -0
- openprocess/gui/flow/page.py +972 -0
- openprocess/gui/flow/templates.py +131 -0
- openprocess/gui/flow/viewers.py +665 -0
- openprocess/gui/items.py +1275 -0
- openprocess/gui/learn/answer_boxes.py +978 -0
- openprocess/gui/learn/concealment.py +91 -0
- openprocess/gui/learn/mode.py +1181 -0
- openprocess/gui/panning.py +241 -0
- openprocess/gui/resources/openprocess-icon.png +0 -0
- openprocess/gui/studio/__init__.py +1 -0
- openprocess/gui/studio/__main__.py +3 -0
- openprocess/gui/studio/app.py +4031 -0
- openprocess/gui/studio/charts.py +115 -0
- openprocess/gui/studio/compare_page.py +487 -0
- openprocess/gui/studio/cpn_page.py +1858 -0
- openprocess/gui/studio/definition_view.py +284 -0
- openprocess/gui/studio/derivation_view.py +421 -0
- openprocess/gui/studio/documents.py +152 -0
- openprocess/gui/studio/dotted_chart.py +1401 -0
- openprocess/gui/studio/file_dialogs.py +143 -0
- openprocess/gui/studio/filter_dialog.py +247 -0
- openprocess/gui/studio/graph_builders.py +176 -0
- openprocess/gui/studio/graph_view.py +682 -0
- openprocess/gui/studio/instances.py +413 -0
- openprocess/gui/studio/log_editor.py +675 -0
- openprocess/gui/studio/log_page.py +800 -0
- openprocess/gui/studio/markdown_view.py +127 -0
- openprocess/gui/studio/mathtext.py +260 -0
- openprocess/gui/studio/ml_highlighter.py +75 -0
- openprocess/gui/studio/model_page.py +760 -0
- openprocess/gui/studio/net_comparison.py +124 -0
- openprocess/gui/studio/notes_overlay.py +275 -0
- openprocess/gui/studio/petri_page.py +844 -0
- openprocess/gui/studio/regions_view.py +502 -0
- openprocess/gui/studio/sidebar.py +149 -0
- openprocess/gui/studio/style.py +503 -0
- openprocess/gui/studio/tool_icons.py +134 -0
- openprocess/gui/studio/updates.py +439 -0
- openprocess/gui/studio/widgets.py +899 -0
- openprocess/gui/studio/workers.py +60 -0
- openprocess/gui/studio/workspace.py +447 -0
- openprocess/gui/theme.py +394 -0
- openprocess/gui/tidy.py +86 -0
- openprocess/io/__init__.py +0 -0
- openprocess/io/cpn_reader.py +389 -0
- openprocess/io/cpn_writer.py +357 -0
- openprocess/learn/__init__.py +23 -0
- openprocess/learn/answers.py +188 -0
- openprocess/learn/checks.py +953 -0
- openprocess/learn/computed.py +1180 -0
- openprocess/learn/context.py +145 -0
- openprocess/learn/exam.py +169 -0
- openprocess/learn/exercise-packs.md +325 -0
- openprocess/learn/importer.py +216 -0
- openprocess/learn/notation.py +474 -0
- openprocess/learn/pack.py +511 -0
- openprocess/learn/sheet.py +296 -0
- openprocess/mining/__init__.py +73 -0
- openprocess/mining/analysis.py +689 -0
- openprocess/mining/columns.py +282 -0
- openprocess/mining/compare_nets.py +246 -0
- openprocess/mining/conformance/__init__.py +0 -0
- openprocess/mining/conformance/alignments.py +263 -0
- openprocess/mining/conformance/quality.py +145 -0
- openprocess/mining/conformance/token_replay.py +252 -0
- openprocess/mining/csv_import.py +222 -0
- openprocess/mining/definitions.py +584 -0
- openprocess/mining/dfg.py +187 -0
- openprocess/mining/discovery/__init__.py +0 -0
- openprocess/mining/discovery/alpha.py +168 -0
- openprocess/mining/discovery/heuristics.py +332 -0
- openprocess/mining/discovery/inductive.py +477 -0
- openprocess/mining/discovery/state_regions.py +62 -0
- openprocess/mining/filtering.py +237 -0
- openprocess/mining/footprint.py +183 -0
- openprocess/mining/invariants.py +191 -0
- openprocess/mining/layout.py +279 -0
- openprocess/mining/log.py +364 -0
- openprocess/mining/petrinet.py +354 -0
- openprocess/mining/playout.py +75 -0
- openprocess/mining/pm4py_bridge.py +82 -0
- openprocess/mining/pnml.py +223 -0
- openprocess/mining/processtree.py +216 -0
- openprocess/mining/regions.py +476 -0
- openprocess/mining/stats.py +160 -0
- openprocess/mining/structure.py +374 -0
- openprocess/mining/transition_system.py +409 -0
- openprocess/mining/xes.py +399 -0
- openprocess/ml/__init__.py +0 -0
- openprocess/ml/ast_nodes.py +332 -0
- openprocess/ml/builtins.py +364 -0
- openprocess/ml/colorsets.py +522 -0
- openprocess/ml/errors.py +60 -0
- openprocess/ml/evaluator.py +754 -0
- openprocess/ml/lexer.py +277 -0
- openprocess/ml/multiset.py +417 -0
- openprocess/ml/parser.py +737 -0
- openprocess/ml/values.py +319 -0
- openprocess/model/__init__.py +0 -0
- openprocess/model/declarations.py +617 -0
- openprocess/model/examples.py +98 -0
- openprocess/model/net.py +701 -0
- openprocess/model/plain.py +192 -0
- openprocess/references.py +280 -0
- openprocess/sim/__init__.py +0 -0
- openprocess/sim/binding.py +620 -0
- openprocess/sim/export.py +66 -0
- openprocess/sim/simulator.py +315 -0
- openprocess/teaching/__init__.py +4 -0
- openprocess/teaching/answers.py +4 -0
- openprocess/teaching/checks.py +5 -0
- openprocess/teaching/pack.py +4 -0
- openprocess/teaching/sheet.py +4 -0
- openprocess-0.7.0.dist-info/METADATA +927 -0
- openprocess-0.7.0.dist-info/RECORD +173 -0
- openprocess-0.7.0.dist-info/WHEEL +5 -0
- openprocess-0.7.0.dist-info/entry_points.txt +6 -0
- openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
`` 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.
|