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,474 @@
|
|
|
1
|
+
"""The notations of the course, read from what a student types and written back.
|
|
2
|
+
|
|
3
|
+
Every answer box beyond a set or a number has a notation the course already
|
|
4
|
+
uses on paper. This module reads each one leniently and shows it back in
|
|
5
|
+
its canonical form, so a check can compare what was typed with what the
|
|
6
|
+
engine computed:
|
|
7
|
+
|
|
8
|
+
=========== =========================================== ===================
|
|
9
|
+
block type typed read as
|
|
10
|
+
=========== =========================================== ===================
|
|
11
|
+
marking ``[p1, p4²]``, ``[p1, p4^2]``, ``p1 + 2p4`` a multiset of places
|
|
12
|
+
markings ``[p1], [p2, p3], [p4]`` a set of markings
|
|
13
|
+
tuple four fields: P, T, F, m₀ sets, pairs, a marking
|
|
14
|
+
ts ``s0 -a-> s1`` lines (see transition_system) a TransitionSystem
|
|
15
|
+
matrix rows of numbers with row and column headers a named matrix
|
|
16
|
+
cut ``→ {a} {b, c, e} {d}`` an operator and groups
|
|
17
|
+
log ``[<a, b>^2, <a, c>]`` a multiset of traces
|
|
18
|
+
tree ``→(a, ×(∧(b, c), e), d)`` a process tree
|
|
19
|
+
alignment two rows, ``a b ≫ c`` over ``a ≫ d c`` moves (log, model)
|
|
20
|
+
replay a table: p, c, m, r per trace numbers per trace
|
|
21
|
+
ranking ``m2 > m1 > m3`` or one name per line an order of names
|
|
22
|
+
=========== =========================================== ===================
|
|
23
|
+
|
|
24
|
+
Nothing here needs Qt: the same readers serve the app, the command line and
|
|
25
|
+
the tests.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from collections import Counter
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
|
|
34
|
+
from . import answers
|
|
35
|
+
from .answers import AnswerSyntaxError
|
|
36
|
+
|
|
37
|
+
SKIP = "≫"
|
|
38
|
+
#: What may stand for ≫ (the "no move") in a typed alignment.
|
|
39
|
+
SKIP_WORDS = {"≫", ">>", "»", "-", "–", "—", "_", "skip", "nomove", "no move"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Markings
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
_COUNT_BEFORE = re.compile(r"^(\d+)\s*[·*x×]?\s*(.+)$")
|
|
46
|
+
_COUNT_AFTER = re.compile(r"^(.+?)\s*(?:\^\s*(\d+)|([²³⁴⁵⁶⁷⁸⁹⁰¹]+)|:\s*(\d+))$")
|
|
47
|
+
_SUPERSCRIPTS = str.maketrans("⁰¹²³⁴⁵⁶⁷⁸⁹", "0123456789")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _strip_brackets(text: str) -> str:
|
|
51
|
+
text = text.strip()
|
|
52
|
+
for latex, plain in (("\\{", "{"), ("\\}", "}"), ("\\emptyset", "∅"), ("$", "")):
|
|
53
|
+
text = text.replace(latex, plain)
|
|
54
|
+
text = text.strip()
|
|
55
|
+
if len(text) >= 2 and text[0] in "[{(" and text[-1] in "]})":
|
|
56
|
+
return text[1:-1].strip()
|
|
57
|
+
return text
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def marking(text: str) -> Counter:
|
|
61
|
+
"""A marking as typed: ``[p1, p4²]``, ``[p1, p4^2]``, ``[p1, 2p4]``,
|
|
62
|
+
``p1 + 2·p4``, ``{p1: 1, p4: 2}``, ``[]`` or ``∅``. Names are compared
|
|
63
|
+
like other answers (case and spaces do not matter)."""
|
|
64
|
+
body = _strip_brackets(text)
|
|
65
|
+
result: Counter = Counter()
|
|
66
|
+
if body in ("", "∅", "-"):
|
|
67
|
+
return result
|
|
68
|
+
for item in re.split(r"[,+;]", body):
|
|
69
|
+
item = item.strip()
|
|
70
|
+
if not item:
|
|
71
|
+
continue
|
|
72
|
+
count = 1
|
|
73
|
+
after = _COUNT_AFTER.match(item)
|
|
74
|
+
if after and (after.group(2) or after.group(3) or after.group(4)):
|
|
75
|
+
item = after.group(1)
|
|
76
|
+
count = int((after.group(2) or after.group(4) or after.group(3).translate(_SUPERSCRIPTS)))
|
|
77
|
+
else:
|
|
78
|
+
before = _COUNT_BEFORE.match(item)
|
|
79
|
+
if before:
|
|
80
|
+
count, item = int(before.group(1)), before.group(2)
|
|
81
|
+
name = answers.name(item)
|
|
82
|
+
if not name:
|
|
83
|
+
raise AnswerSyntaxError(f"“{item}” is not a place")
|
|
84
|
+
if count:
|
|
85
|
+
result[name] += count
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def show_marking(counts) -> str:
|
|
90
|
+
"""``[p1, p4^2]`` (the textbook's notation); ``[]`` when empty."""
|
|
91
|
+
parts = []
|
|
92
|
+
for name in sorted(counts, key=answers.name if all(isinstance(k, str) for k in counts) else str):
|
|
93
|
+
n = counts[name]
|
|
94
|
+
if n:
|
|
95
|
+
parts.append(f"{name}^{n}" if n != 1 else str(name))
|
|
96
|
+
return "[" + ", ".join(parts) + "]"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def marking_key(counts) -> frozenset:
|
|
100
|
+
"""A marking as something hashable, for sets of markings."""
|
|
101
|
+
return frozenset((answers.name(k), v) for k, v in counts.items() if v)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def markings(text: str) -> frozenset:
|
|
105
|
+
"""A set of markings: ``[p1], [p2, p3], [p4]`` (also inside ``{ }``)."""
|
|
106
|
+
text = text.strip()
|
|
107
|
+
for latex, plain in (("\\{", "{"), ("\\}", "}"), ("$", "")):
|
|
108
|
+
text = text.replace(latex, plain)
|
|
109
|
+
if text.startswith("{") and text.endswith("}") and "[" in text:
|
|
110
|
+
text = text[1:-1]
|
|
111
|
+
found = re.findall(r"\[[^\[\]]*\]", text)
|
|
112
|
+
if not found:
|
|
113
|
+
if text.strip() in ("", "∅", "{}"):
|
|
114
|
+
return frozenset()
|
|
115
|
+
raise AnswerSyntaxError("write each marking in square brackets, like [p1, p2^2]")
|
|
116
|
+
return frozenset(marking_key(marking(m)) for m in found)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def show_markings(keys) -> str:
|
|
120
|
+
shown = sorted(show_marking(dict(k)) for k in keys)
|
|
121
|
+
return ", ".join(shown) if shown else "∅"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Cuts and process trees
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
OPERATORS = {"→": "→", "->": "→", "seq": "→", "sequence": "→", "x": "×", "×": "×", "xor": "×",
|
|
128
|
+
"choice": "×", "exclusive": "×", "^": "∧", "∧": "∧", "and": "∧", "parallel": "∧",
|
|
129
|
+
"par": "∧", "concurrent": "∧", "↺": "↺", "loop": "↺", "*": "↺", "redo": "↺"}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _operator(token: str) -> str:
|
|
133
|
+
key = token.strip().casefold()
|
|
134
|
+
if key in OPERATORS:
|
|
135
|
+
return OPERATORS[key]
|
|
136
|
+
raise AnswerSyntaxError(f"“{token}” is not an operator (use →, ×, ∧ or ↺, or write "
|
|
137
|
+
"sequence, xor, and, loop)")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def cut(text: str) -> tuple[str, tuple[frozenset, ...]]:
|
|
141
|
+
"""A cut as typed: ``→ {a} {b, c, e} {d}``, ``sequence: {a}, {b,c,e}, {d}`` or
|
|
142
|
+
``→({a}, {b, c, e}, {d})``: the operator and its groups, in order."""
|
|
143
|
+
text = text.strip().replace("\\{", "{").replace("\\}", "}").replace("$", "")
|
|
144
|
+
match = re.match(r"^\s*([^\s{(:]+)\s*[:(]?\s*(.*?)\)?\s*$", text, re.S)
|
|
145
|
+
if not match:
|
|
146
|
+
raise AnswerSyntaxError("write the operator first, then the groups in braces")
|
|
147
|
+
operator = _operator(match.group(1))
|
|
148
|
+
groups = re.findall(r"\{[^{}]*\}", match.group(2))
|
|
149
|
+
if not groups:
|
|
150
|
+
raise AnswerSyntaxError("write each group of the cut in braces, like {b, c}")
|
|
151
|
+
parsed = tuple(frozenset(answers.name(n) for n in _strip_brackets(g).split(",") if n.strip())
|
|
152
|
+
for g in groups)
|
|
153
|
+
if any(not g for g in parsed):
|
|
154
|
+
raise AnswerSyntaxError("a group of the cut is empty")
|
|
155
|
+
return operator, parsed
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def show_cut(value) -> str:
|
|
159
|
+
operator, groups = value
|
|
160
|
+
return operator + " " + " ".join("{" + ", ".join(sorted(g)) + "}" for g in groups)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class Tree:
|
|
165
|
+
"""A process tree as typed (compared with the engine's, order of the
|
|
166
|
+
children of × and ∧ ignored)."""
|
|
167
|
+
operator: str | None = None
|
|
168
|
+
children: list["Tree"] = field(default_factory=list)
|
|
169
|
+
label: str | None = None
|
|
170
|
+
|
|
171
|
+
def key(self):
|
|
172
|
+
if self.operator is None:
|
|
173
|
+
return ("leaf", self.label)
|
|
174
|
+
inner = [c.key() for c in self.children]
|
|
175
|
+
if self.operator in ("×", "∧"):
|
|
176
|
+
inner = sorted(inner, key=repr)
|
|
177
|
+
return (self.operator, tuple(inner))
|
|
178
|
+
|
|
179
|
+
def __str__(self) -> str:
|
|
180
|
+
if self.operator is None:
|
|
181
|
+
return "τ" if self.label is None else self.label
|
|
182
|
+
return f"{self.operator}(" + ", ".join(map(str, self.children)) + ")"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
_TAU = {"τ", "tau", "t", "silent", "skip"}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def tree(text: str) -> Tree:
|
|
189
|
+
"""``→(a, ×(∧(b, c), e), d)``; also ``seq(a, xor(and(b, c), e), d)``."""
|
|
190
|
+
text = text.strip().replace("$", "").replace("\\tau", "τ")
|
|
191
|
+
tokens = [t for t in re.findall(r"[(),]|[^(),\s][^(),]*", text)]
|
|
192
|
+
tokens = [t.strip() for t in tokens if t.strip()]
|
|
193
|
+
position = 0
|
|
194
|
+
|
|
195
|
+
def parse() -> Tree:
|
|
196
|
+
nonlocal position
|
|
197
|
+
if position >= len(tokens):
|
|
198
|
+
raise AnswerSyntaxError("the tree ends too soon")
|
|
199
|
+
token = tokens[position]
|
|
200
|
+
position += 1
|
|
201
|
+
if position < len(tokens) and tokens[position] == "(":
|
|
202
|
+
operator = _operator(token)
|
|
203
|
+
position += 1
|
|
204
|
+
children = []
|
|
205
|
+
while True:
|
|
206
|
+
children.append(parse())
|
|
207
|
+
if position >= len(tokens):
|
|
208
|
+
raise AnswerSyntaxError("a “)” is missing")
|
|
209
|
+
if tokens[position] == ",":
|
|
210
|
+
position += 1
|
|
211
|
+
continue
|
|
212
|
+
if tokens[position] == ")":
|
|
213
|
+
position += 1
|
|
214
|
+
break
|
|
215
|
+
raise AnswerSyntaxError("a comma is missing between two children")
|
|
216
|
+
return Tree(operator, children)
|
|
217
|
+
if token in "(),":
|
|
218
|
+
raise AnswerSyntaxError(f"unexpected “{token}”")
|
|
219
|
+
name = answers.name(token)
|
|
220
|
+
return Tree(label=None if name in _TAU else name)
|
|
221
|
+
|
|
222
|
+
result = parse()
|
|
223
|
+
if position < len(tokens):
|
|
224
|
+
raise AnswerSyntaxError(f"unexpected “{tokens[position]}” after the tree")
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def tree_from_engine(node) -> Tree:
|
|
229
|
+
"""A :class:`openprocess.mining.processtree.ProcessTree` as a :class:`Tree`."""
|
|
230
|
+
if node.is_leaf:
|
|
231
|
+
return Tree(label=None if node.label is None else answers.name(node.label))
|
|
232
|
+
return Tree(node.operator.value, [tree_from_engine(c) for c in node.children])
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# Logs
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
def log(text: str) -> Counter:
|
|
239
|
+
"""A log in the course's notation, names normalised: ``[<a,b>^2, <a,c>]``."""
|
|
240
|
+
from ..mining.log import parse_simple_log
|
|
241
|
+
try:
|
|
242
|
+
parsed = parse_simple_log(text)
|
|
243
|
+
except ValueError as error:
|
|
244
|
+
raise AnswerSyntaxError(str(error)) from None
|
|
245
|
+
return Counter({tuple(answers.name(a) for a in trace): n for trace, n in parsed.items() if n})
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def show_log(counts) -> str:
|
|
249
|
+
from ..mining.log import format_simple_log
|
|
250
|
+
return format_simple_log(Counter(counts))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# Matrices
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
@dataclass
|
|
257
|
+
class Matrix:
|
|
258
|
+
"""Rows and columns by name, with a value in every cell."""
|
|
259
|
+
rows: list[str]
|
|
260
|
+
columns: list[str]
|
|
261
|
+
cells: dict[tuple[str, str], float]
|
|
262
|
+
|
|
263
|
+
def get(self, row: str, column: str):
|
|
264
|
+
return self.cells.get((answers.name(row), answers.name(column)))
|
|
265
|
+
|
|
266
|
+
def normalised(self) -> "Matrix":
|
|
267
|
+
return Matrix([answers.name(r) for r in self.rows], [answers.name(c) for c in self.columns],
|
|
268
|
+
{(answers.name(r), answers.name(c)): v for (r, c), v in self.cells.items()})
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _number(token: str) -> float:
|
|
272
|
+
token = token.strip().replace("−", "-").replace("–", "-")
|
|
273
|
+
if token in ("", ".", "·"):
|
|
274
|
+
return 0.0
|
|
275
|
+
return answers.number(token)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def matrix(text: str) -> Matrix:
|
|
279
|
+
"""A matrix typed as rows of numbers with headers::
|
|
280
|
+
|
|
281
|
+
. a b c
|
|
282
|
+
p1 -1 1 0
|
|
283
|
+
p2 0 -1 1
|
|
284
|
+
|
|
285
|
+
The first line names the columns (its first cell may be empty or a dot);
|
|
286
|
+
each other line starts with its row's name. Cells are separated by
|
|
287
|
+
spaces, tabs, commas or ``|`` (a Markdown table works too)."""
|
|
288
|
+
lines = [ln.strip().strip("|") for ln in text.strip().splitlines()]
|
|
289
|
+
lines = [ln for ln in lines if ln.strip() and not re.fullmatch(r"[\s|:\-]+", ln)]
|
|
290
|
+
if len(lines) < 2:
|
|
291
|
+
raise AnswerSyntaxError("write the column names on the first line and one row per line")
|
|
292
|
+
|
|
293
|
+
def split(line: str) -> list[str]:
|
|
294
|
+
if "|" in line:
|
|
295
|
+
return [c.strip() for c in line.split("|")]
|
|
296
|
+
if "\t" in line:
|
|
297
|
+
return [c.strip() for c in line.split("\t")]
|
|
298
|
+
if "," in line:
|
|
299
|
+
return [c.strip() for c in line.split(",")]
|
|
300
|
+
return line.split()
|
|
301
|
+
|
|
302
|
+
header = split(lines[0])
|
|
303
|
+
if header and header[0] in ("", ".", "·", "-", "x", "\\"):
|
|
304
|
+
header = header[1:]
|
|
305
|
+
columns = [answers.name(c) for c in header]
|
|
306
|
+
rows, cells = [], {}
|
|
307
|
+
for line in lines[1:]:
|
|
308
|
+
parts = split(line)
|
|
309
|
+
if len(parts) == len(columns) + 1:
|
|
310
|
+
name, values = answers.name(parts[0]), parts[1:]
|
|
311
|
+
elif len(parts) == len(columns):
|
|
312
|
+
name, values = f"row {len(rows) + 1}", parts
|
|
313
|
+
else:
|
|
314
|
+
raise AnswerSyntaxError(f"the row “{line}” has {len(parts) - 1} values for "
|
|
315
|
+
f"{len(columns)} columns")
|
|
316
|
+
rows.append(name)
|
|
317
|
+
for column, value in zip(columns, values):
|
|
318
|
+
try:
|
|
319
|
+
cells[(name, column)] = _number(value)
|
|
320
|
+
except (ValueError, ZeroDivisionError):
|
|
321
|
+
raise AnswerSyntaxError(f"“{value}” in row {name} is not a number") from None
|
|
322
|
+
return Matrix(rows, columns, cells)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def show_matrix(value: Matrix) -> str:
|
|
326
|
+
rows, columns = value.rows, value.columns
|
|
327
|
+
width = max([len(str(c)) for c in columns] + [len(str(r)) for r in rows] + [3])
|
|
328
|
+
|
|
329
|
+
def fmt(x) -> str:
|
|
330
|
+
return f"{x:g}" if isinstance(x, float) else str(x)
|
|
331
|
+
lines = [" " * (width + 1) + " ".join(str(c).rjust(width) for c in columns)]
|
|
332
|
+
for row in rows:
|
|
333
|
+
lines.append(str(row).ljust(width) + " " +
|
|
334
|
+
" ".join(fmt(value.cells.get((row, c), 0)).rjust(width) for c in columns))
|
|
335
|
+
return "\n".join(lines)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# Alignments and replay tables
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
def alignment(text: str) -> tuple[list[str], list[str]]:
|
|
342
|
+
"""Two rows of moves: the log row over the model row. Cells are
|
|
343
|
+
separated by spaces, ``|`` or commas; ``≫`` (also ``>>`` or ``-``) is no
|
|
344
|
+
move. Returns ``(log moves, model moves)`` with names normalised and
|
|
345
|
+
``≫`` for no move."""
|
|
346
|
+
text = text.replace("$", "").replace("\\gg", "≫")
|
|
347
|
+
lines = [ln for ln in text.strip().splitlines() if ln.strip()
|
|
348
|
+
and not re.fullmatch(r"[\s|:\-]+", ln)]
|
|
349
|
+
if len(lines) != 2:
|
|
350
|
+
raise AnswerSyntaxError("write the alignment as two lines: the log moves, then the "
|
|
351
|
+
"model moves")
|
|
352
|
+
|
|
353
|
+
def cells(line: str) -> list[str]:
|
|
354
|
+
line = line.strip().strip("|")
|
|
355
|
+
parts = [c.strip() for c in re.split(r"\s*\|\s*|,", line)] if ("|" in line or "," in line) \
|
|
356
|
+
else line.split()
|
|
357
|
+
parts = [p for p in parts if p != ""]
|
|
358
|
+
if parts and answers.name(parts[0]) in ("log", "model", "trace", "net", "l", "m"):
|
|
359
|
+
parts = parts[1:]
|
|
360
|
+
return [SKIP if p.casefold() in SKIP_WORDS else answers.name(p) for p in parts]
|
|
361
|
+
top, bottom = cells(lines[0]), cells(lines[1])
|
|
362
|
+
if len(top) != len(bottom):
|
|
363
|
+
raise AnswerSyntaxError(f"the rows have different lengths ({len(top)} and {len(bottom)} "
|
|
364
|
+
"moves): write ≫ where there is no move")
|
|
365
|
+
if any(a == SKIP and b == SKIP for a, b in zip(top, bottom)):
|
|
366
|
+
raise AnswerSyntaxError("a column with ≫ in both rows is not a move")
|
|
367
|
+
return top, bottom
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def show_alignment(moves) -> str:
|
|
371
|
+
"""Two aligned rows from ``(log, model)`` pairs."""
|
|
372
|
+
pairs = list(moves)
|
|
373
|
+
width = [max(len(str(a)), len(str(b)), 1) for a, b in pairs]
|
|
374
|
+
top = " ".join(str(a).ljust(w) for (a, _), w in zip(pairs, width))
|
|
375
|
+
bottom = " ".join(str(b).ljust(w) for (_, b), w in zip(pairs, width))
|
|
376
|
+
return top.rstrip() + "\n" + bottom.rstrip()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
REPLAY_COLUMNS = ("p", "c", "m", "r")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def replay_table(value) -> dict[str, dict[str, float]]:
|
|
383
|
+
"""A replay table as the editor stores it: ``{trace text: {p, c, m, r}}``
|
|
384
|
+
with the trace keys normalised. A string is read as lines of
|
|
385
|
+
``<a, b, c>: p c m r`` (or ``<a, b, c> | p | c | m | r``)."""
|
|
386
|
+
if isinstance(value, dict):
|
|
387
|
+
result = {}
|
|
388
|
+
for key, row in value.items():
|
|
389
|
+
if not isinstance(row, dict):
|
|
390
|
+
continue
|
|
391
|
+
trace = tuple(answers.trace(str(key)))
|
|
392
|
+
result[trace] = {}
|
|
393
|
+
for column in REPLAY_COLUMNS:
|
|
394
|
+
cell = row.get(column, "")
|
|
395
|
+
if cell not in ("", None):
|
|
396
|
+
try:
|
|
397
|
+
result[trace][column] = answers.number(str(cell))
|
|
398
|
+
except (ValueError, ZeroDivisionError):
|
|
399
|
+
raise AnswerSyntaxError(f"“{cell}” is not a number") from None
|
|
400
|
+
return result
|
|
401
|
+
result = {}
|
|
402
|
+
for line in str(value).strip().splitlines():
|
|
403
|
+
if not line.strip():
|
|
404
|
+
continue
|
|
405
|
+
match = re.match(r"^\s*([<⟨].*?[>⟩])\s*[:|]?\s*(.*)$", line)
|
|
406
|
+
if not match:
|
|
407
|
+
raise AnswerSyntaxError(f"start each line with the trace in ⟨ ⟩: “{line}”")
|
|
408
|
+
numbers = [n for n in re.split(r"[\s|,]+", match.group(2).strip()) if n]
|
|
409
|
+
trace = tuple(answers.trace(match.group(1)))
|
|
410
|
+
result[trace] = {}
|
|
411
|
+
for column, cell in zip(REPLAY_COLUMNS, numbers):
|
|
412
|
+
result[trace][column] = answers.number(cell)
|
|
413
|
+
return result
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# Rankings
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
def ranking(value) -> list[str]:
|
|
420
|
+
"""An order of names, best first: a list, ``m2 > m1 > m3``, ``m2, m1, m3``
|
|
421
|
+
or one name per line (``1. m2``)."""
|
|
422
|
+
if isinstance(value, list):
|
|
423
|
+
names = [str(v) for v in value]
|
|
424
|
+
else:
|
|
425
|
+
text = str(value).strip()
|
|
426
|
+
if ">" in text:
|
|
427
|
+
names = text.split(">")
|
|
428
|
+
elif "\n" in text:
|
|
429
|
+
names = text.splitlines()
|
|
430
|
+
else:
|
|
431
|
+
names = text.split(",")
|
|
432
|
+
names = [re.sub(r"^\s*\d+[.)]\s*", "", n) for n in names]
|
|
433
|
+
names = [answers.name(n.replace(".pnml", "")) for n in names if n.strip()]
|
|
434
|
+
if len(set(names)) != len(names):
|
|
435
|
+
raise AnswerSyntaxError("a name appears twice in the ranking")
|
|
436
|
+
return names
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
# The tuple (P, T, F, m0)
|
|
441
|
+
# ---------------------------------------------------------------------------
|
|
442
|
+
TUPLE_FIELDS = ("P", "T", "F", "m0")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def net_tuple(value) -> dict:
|
|
446
|
+
"""The four parts of ``(P, T, F, m0)`` as the editor stores them
|
|
447
|
+
(``{"P": "...", "T": "...", "F": "...", "m0": "..."}``), read: ``P`` and
|
|
448
|
+
``T`` as sets of names, ``F`` as a set of pairs, ``m0`` as a marking."""
|
|
449
|
+
if isinstance(value, str):
|
|
450
|
+
value = tuple_text(value)
|
|
451
|
+
parts = {k: (value.get(k) or "") for k in TUPLE_FIELDS}
|
|
452
|
+
read = {}
|
|
453
|
+
read["P"] = answers.as_set(parts["P"]) if parts["P"].strip() else frozenset()
|
|
454
|
+
read["T"] = answers.as_set(parts["T"]) if parts["T"].strip() else frozenset()
|
|
455
|
+
arcs = answers.as_set(parts["F"]) if parts["F"].strip() else frozenset()
|
|
456
|
+
pairs = set()
|
|
457
|
+
for item in arcs:
|
|
458
|
+
if isinstance(item, tuple) and len(item) == 2 and all(isinstance(x, str) for x in item):
|
|
459
|
+
pairs.add(item)
|
|
460
|
+
else:
|
|
461
|
+
raise AnswerSyntaxError("write F as pairs, like (p1, a), (a, p2)")
|
|
462
|
+
read["F"] = frozenset(pairs)
|
|
463
|
+
read["m0"] = marking_key(marking(parts["m0"])) if parts["m0"].strip() else frozenset()
|
|
464
|
+
return read
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def tuple_text(text: str) -> dict:
|
|
468
|
+
"""``P = {p1, p2}; T = {a, b}; F = {(p1, a), …}; m0 = [p1]`` on one or more lines."""
|
|
469
|
+
found = {}
|
|
470
|
+
for key in TUPLE_FIELDS:
|
|
471
|
+
pattern = rf"(?:^|[;\n])\s*{re.escape(key)}\s*[=:]\s*(.*?)(?=(?:[;\n]\s*(?:P|T|F|m0)\s*[=:])|$)"
|
|
472
|
+
match = re.search(pattern, text.replace("m_0", "m0").replace("M0", "m0"), re.S | re.I)
|
|
473
|
+
found[key] = match.group(1).strip() if match else ""
|
|
474
|
+
return found
|