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,357 @@
|
|
|
1
|
+
"""Writer for CPN Tools ``.cpn`` files.
|
|
2
|
+
|
|
3
|
+
The goal is a file that CPN Tools 4.0 and CPN IDE will both open without
|
|
4
|
+
complaint, so we emit the full element set they expect -- including the
|
|
5
|
+
graphical attribute groups that are structurally required even when they carry
|
|
6
|
+
only defaults.
|
|
7
|
+
|
|
8
|
+
Round-trip policy
|
|
9
|
+
-----------------
|
|
10
|
+
Everything this implementation models (declarations, pages, places,
|
|
11
|
+
transitions, arcs, their inscriptions and their layout) is written from the
|
|
12
|
+
model objects, so edits are reflected. Everything it does *not* model
|
|
13
|
+
(monitors, binders, workspace options, the hierarchy index) is copied verbatim
|
|
14
|
+
from the tree the file was read from, when there is one. That combination
|
|
15
|
+
means:
|
|
16
|
+
|
|
17
|
+
* a file we opened and saved without editing is semantically identical;
|
|
18
|
+
* a file we edited keeps its monitors and workspace settings;
|
|
19
|
+
* a file we created from scratch gets a minimal but valid set of those
|
|
20
|
+
sections.
|
|
21
|
+
|
|
22
|
+
Colour set declarations are written as ``<layout>`` text plus the minimum
|
|
23
|
+
structured markup CPN Tools needs to recognise the kind. CPN Tools re-derives
|
|
24
|
+
its internal representation from the layout text on load, so this is the
|
|
25
|
+
faithful thing to write.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import xml.etree.ElementTree as ElementTree
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from xml.dom import minidom
|
|
33
|
+
|
|
34
|
+
from ..model.net import Arc, CPNet, Graphics, Page, Place, Transition
|
|
35
|
+
|
|
36
|
+
#: The DOCTYPE CPN Tools writes. Kept byte-identical so that tools which
|
|
37
|
+
#: sniff the header recognise our output.
|
|
38
|
+
DOCTYPE = (
|
|
39
|
+
'<!DOCTYPE workspaceElements PUBLIC "-//CPN//DTD CPNXML 1.0//EN" '
|
|
40
|
+
'"http://cpntools.org/DTD/6/cpn.dtd">'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_ID_SEQUENCE = [0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _fresh_id() -> str:
|
|
47
|
+
"""Ids for elements that need one but that the model does not track
|
|
48
|
+
(annotation boxes, type labels, and so on)."""
|
|
49
|
+
_ID_SEQUENCE[0] += 1
|
|
50
|
+
return f"IDW{_ID_SEQUENCE[0]}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Attribute groups
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
def _add_graphics(parent: ElementTree.Element, graphics: Graphics) -> None:
|
|
57
|
+
"""Write the four attribute elements every graphical object carries."""
|
|
58
|
+
ElementTree.SubElement(parent, "posattr", x=f"{graphics.x:.6f}", y=f"{graphics.y:.6f}")
|
|
59
|
+
ElementTree.SubElement(
|
|
60
|
+
parent, "fillattr", colour=graphics.fill_colour, pattern="", filled="false"
|
|
61
|
+
)
|
|
62
|
+
ElementTree.SubElement(
|
|
63
|
+
parent, "lineattr", colour=graphics.line_colour,
|
|
64
|
+
thick=str(int(graphics.line_width)), type="Solid",
|
|
65
|
+
)
|
|
66
|
+
ElementTree.SubElement(parent, "textattr", colour=graphics.text_colour, bold="false")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _at(graphics: Graphics, tag: str, default_x: float, default_y: float) -> tuple[float, float]:
|
|
70
|
+
"""Absolute position for a node's inscription: its stored offset (or a
|
|
71
|
+
default offset) added to the node's own position."""
|
|
72
|
+
dx, dy = graphics.label_offsets.get(tag, (default_x, default_y))
|
|
73
|
+
return graphics.x + dx, graphics.y + dy
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _add_annotation(parent: ElementTree.Element, tag: str, text: str,
|
|
77
|
+
x: float = 0.0, y: float = 0.0) -> ElementTree.Element:
|
|
78
|
+
"""Write an inscription element such as ``<initmark>`` or ``<cond>``.
|
|
79
|
+
|
|
80
|
+
CPN Tools represents every inscription as a small graphical object with its
|
|
81
|
+
own id, position and a ``<text>`` payload -- even an empty one, which is
|
|
82
|
+
why we always emit the element rather than skipping it when the text is
|
|
83
|
+
blank.
|
|
84
|
+
"""
|
|
85
|
+
element = ElementTree.SubElement(parent, tag, id=_fresh_id())
|
|
86
|
+
_add_graphics(element, Graphics(x=x, y=y))
|
|
87
|
+
text_element = ElementTree.SubElement(element, "text")
|
|
88
|
+
text_element.text = text
|
|
89
|
+
return element
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Declarations
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
def _structured_colour_set(parent: ElementTree.Element, source: str) -> None:
|
|
96
|
+
"""Emit the minimal structured markup for a colour set declaration.
|
|
97
|
+
|
|
98
|
+
CPN Tools reads the ``<layout>`` text authoritatively, but its parser still
|
|
99
|
+
expects one of the recognised kind elements to be present. We infer the
|
|
100
|
+
kind from the declaration text -- which we know is well-formed, because the
|
|
101
|
+
model compiled it -- and emit that one element.
|
|
102
|
+
"""
|
|
103
|
+
body = source.split("=", 1)[1] if "=" in source else source
|
|
104
|
+
body = body.strip().rstrip(";").strip()
|
|
105
|
+
if body.endswith("timed"):
|
|
106
|
+
body = body[: -len("timed")].strip()
|
|
107
|
+
|
|
108
|
+
first_word = body.split()[0] if body.split() else "unit"
|
|
109
|
+
simple = {"unit", "bool", "int", "intinf", "real", "string"}
|
|
110
|
+
if first_word in simple:
|
|
111
|
+
ElementTree.SubElement(parent, first_word)
|
|
112
|
+
elif first_word == "with":
|
|
113
|
+
enumeration = ElementTree.SubElement(parent, "enum")
|
|
114
|
+
for constant in body[len("with"):].split("|"):
|
|
115
|
+
identifier = ElementTree.SubElement(enumeration, "id")
|
|
116
|
+
identifier.text = constant.strip()
|
|
117
|
+
elif first_word in ("product", "record", "list", "union", "index", "subset"):
|
|
118
|
+
ElementTree.SubElement(parent, first_word)
|
|
119
|
+
else:
|
|
120
|
+
ElementTree.SubElement(parent, "alias")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _write_globbox(cpnet: ElementTree.Element, net: CPNet) -> None:
|
|
124
|
+
"""Write all declarations into a single block.
|
|
125
|
+
|
|
126
|
+
CPN Tools groups declarations into named blocks purely for display. We
|
|
127
|
+
write one block called "Declarations" and keep the original ordering, which
|
|
128
|
+
is what determines whether forward references resolve.
|
|
129
|
+
"""
|
|
130
|
+
globbox = ElementTree.SubElement(cpnet, "globbox")
|
|
131
|
+
block = ElementTree.SubElement(globbox, "block", id=_fresh_id())
|
|
132
|
+
block_id = ElementTree.SubElement(block, "id")
|
|
133
|
+
block_id.text = "Declarations"
|
|
134
|
+
|
|
135
|
+
for name, source in net.declarations.colour_set_sources:
|
|
136
|
+
colour = ElementTree.SubElement(block, "color", id=_fresh_id())
|
|
137
|
+
identifier = ElementTree.SubElement(colour, "id")
|
|
138
|
+
identifier.text = name
|
|
139
|
+
_structured_colour_set(colour, source)
|
|
140
|
+
if source.rstrip().rstrip(";").rstrip().endswith("timed"):
|
|
141
|
+
ElementTree.SubElement(colour, "timed")
|
|
142
|
+
layout = ElementTree.SubElement(colour, "layout")
|
|
143
|
+
layout.text = source
|
|
144
|
+
|
|
145
|
+
# Group variables that share a declaration line back together, so the file
|
|
146
|
+
# reads the way the modeller wrote it.
|
|
147
|
+
seen_sources: set[str] = set()
|
|
148
|
+
for _name, colour_set_name, source in net.declarations.variable_sources:
|
|
149
|
+
if source in seen_sources:
|
|
150
|
+
continue
|
|
151
|
+
seen_sources.add(source)
|
|
152
|
+
variable = ElementTree.SubElement(block, "var", id=_fresh_id())
|
|
153
|
+
type_element = ElementTree.SubElement(variable, "type")
|
|
154
|
+
type_id = ElementTree.SubElement(type_element, "id")
|
|
155
|
+
type_id.text = colour_set_name
|
|
156
|
+
for name, other_colour_set, other_source in net.declarations.variable_sources:
|
|
157
|
+
if other_source == source:
|
|
158
|
+
identifier = ElementTree.SubElement(variable, "id")
|
|
159
|
+
identifier.text = name
|
|
160
|
+
layout = ElementTree.SubElement(variable, "layout")
|
|
161
|
+
layout.text = source
|
|
162
|
+
|
|
163
|
+
for source in net.declarations.ml_sources:
|
|
164
|
+
ml = ElementTree.SubElement(block, "ml", id=_fresh_id())
|
|
165
|
+
ml.text = source
|
|
166
|
+
layout = ElementTree.SubElement(ml, "layout")
|
|
167
|
+
layout.text = source
|
|
168
|
+
|
|
169
|
+
for name, initial in net.declarations.globref_sources:
|
|
170
|
+
globref = ElementTree.SubElement(block, "globref", id=_fresh_id())
|
|
171
|
+
identifier = ElementTree.SubElement(globref, "id")
|
|
172
|
+
identifier.text = name
|
|
173
|
+
ml = ElementTree.SubElement(globref, "ml")
|
|
174
|
+
ml.text = initial
|
|
175
|
+
layout = ElementTree.SubElement(globref, "layout")
|
|
176
|
+
layout.text = f"globref {name} = {initial};"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Net elements
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
def _write_place(page_element: ElementTree.Element, place: Place) -> None:
|
|
183
|
+
element = ElementTree.SubElement(page_element, "place", id=place.id)
|
|
184
|
+
_add_graphics(element, place.graphics)
|
|
185
|
+
|
|
186
|
+
text = ElementTree.SubElement(element, "text")
|
|
187
|
+
text.text = place.name
|
|
188
|
+
|
|
189
|
+
ElementTree.SubElement(
|
|
190
|
+
element, "ellipse",
|
|
191
|
+
w=f"{place.graphics.width:.6f}", h=f"{place.graphics.height:.6f}",
|
|
192
|
+
)
|
|
193
|
+
# The token and marking elements position the little circle and the count
|
|
194
|
+
# that CPN Tools draws next to a marked place (offsets, kept from the file).
|
|
195
|
+
offsets = place.graphics.label_offsets
|
|
196
|
+
token_x, token_y = offsets.get("token", (-10.0, 0.0))
|
|
197
|
+
ElementTree.SubElement(element, "token", x=f"{token_x:.6f}", y=f"{token_y:.6f}")
|
|
198
|
+
marking_x, marking_y = offsets.get("marking", (0.0, 0.0))
|
|
199
|
+
marking = ElementTree.SubElement(
|
|
200
|
+
element, "marking", x=f"{marking_x:.6f}", y=f"{marking_y:.6f}",
|
|
201
|
+
hidden="true" if place.graphics.marking_hidden else "false",
|
|
202
|
+
)
|
|
203
|
+
ElementTree.SubElement(
|
|
204
|
+
marking, "snap", snap_id="0",
|
|
205
|
+
**{"anchor.horizontal": "0", "anchor.vertical": "0"},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Inscriptions go where they were (relative to the place, so they follow
|
|
209
|
+
# it when it is moved), or at CPN Tools' usual spots for new places.
|
|
210
|
+
_add_annotation(element, "type", place.colour_set_name,
|
|
211
|
+
*_at(place.graphics, "type", 30.0, -20.0))
|
|
212
|
+
_add_annotation(element, "initmark", place.initial_marking_text,
|
|
213
|
+
*_at(place.graphics, "initmark", 35.0, 20.0))
|
|
214
|
+
|
|
215
|
+
if place.port_type:
|
|
216
|
+
ElementTree.SubElement(element, "port", id=_fresh_id(), type=place.port_type)
|
|
217
|
+
if place.fusion_group:
|
|
218
|
+
ElementTree.SubElement(
|
|
219
|
+
element, "fusioninfo", id=_fresh_id(), name=place.fusion_group
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _write_transition(page_element: ElementTree.Element, transition: Transition) -> None:
|
|
224
|
+
element = ElementTree.SubElement(
|
|
225
|
+
page_element, "trans", id=transition.id, explicit="false"
|
|
226
|
+
)
|
|
227
|
+
_add_graphics(element, transition.graphics)
|
|
228
|
+
|
|
229
|
+
text = ElementTree.SubElement(element, "text")
|
|
230
|
+
text.text = transition.name
|
|
231
|
+
|
|
232
|
+
ElementTree.SubElement(
|
|
233
|
+
element, "box",
|
|
234
|
+
w=f"{transition.graphics.width:.6f}", h=f"{transition.graphics.height:.6f}",
|
|
235
|
+
)
|
|
236
|
+
ElementTree.SubElement(element, "binding", x="7.200000", y="-3.000000")
|
|
237
|
+
|
|
238
|
+
graphics = transition.graphics
|
|
239
|
+
_add_annotation(element, "cond", transition.guard_text, *_at(graphics, "cond", -40.0, 20.0))
|
|
240
|
+
_add_annotation(element, "time", transition.time_text, *_at(graphics, "time", 40.0, 20.0))
|
|
241
|
+
_add_annotation(element, "code", transition.code_text, *_at(graphics, "code", 40.0, -20.0))
|
|
242
|
+
_add_annotation(element, "priority", transition.priority_text,
|
|
243
|
+
*_at(graphics, "priority", -40.0, -20.0))
|
|
244
|
+
|
|
245
|
+
if transition.is_substitution:
|
|
246
|
+
portsock = "".join(
|
|
247
|
+
f"({socket},{port})" for socket, port in transition.port_assignments.items()
|
|
248
|
+
)
|
|
249
|
+
ElementTree.SubElement(
|
|
250
|
+
element, "subst",
|
|
251
|
+
subpage=transition.substitution_subpage or "",
|
|
252
|
+
portsock=portsock,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _write_arc(page_element: ElementTree.Element, arc: Arc, order: int,
|
|
257
|
+
page: Page | None = None) -> None:
|
|
258
|
+
element = ElementTree.SubElement(
|
|
259
|
+
page_element, "arc", id=arc.id, orientation=arc.orientation, order=str(order)
|
|
260
|
+
)
|
|
261
|
+
_add_graphics(element, arc.graphics)
|
|
262
|
+
ElementTree.SubElement(element, "arrowattr", headsize="1.200000", currentcyckle="2")
|
|
263
|
+
ElementTree.SubElement(element, "transend", idref=arc.transition_id)
|
|
264
|
+
ElementTree.SubElement(element, "placeend", idref=arc.place_id)
|
|
265
|
+
|
|
266
|
+
for serial, (x, y) in enumerate(arc.bendpoints, start=1):
|
|
267
|
+
bendpoint = ElementTree.SubElement(
|
|
268
|
+
element, "bendpoint", id=_fresh_id(), serial=str(serial)
|
|
269
|
+
)
|
|
270
|
+
_add_graphics(bendpoint, Graphics(x=x, y=y))
|
|
271
|
+
|
|
272
|
+
# Arc inscriptions are stored as absolute positions; new arcs get theirs
|
|
273
|
+
# halfway between the two nodes.
|
|
274
|
+
position = arc.graphics.label_offsets.get("annot")
|
|
275
|
+
if position is None and page is not None:
|
|
276
|
+
place, transition = page.place(arc.place_id), page.transition(arc.transition_id)
|
|
277
|
+
if place is not None and transition is not None:
|
|
278
|
+
position = ((place.graphics.x + transition.graphics.x) / 2 + 8,
|
|
279
|
+
(place.graphics.y + transition.graphics.y) / 2 + 8)
|
|
280
|
+
x, y = position if position is not None else (arc.graphics.x, arc.graphics.y)
|
|
281
|
+
_add_annotation(element, "annot", arc.expression_text, x=x, y=y)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _write_page(cpnet: ElementTree.Element, page: Page) -> None:
|
|
285
|
+
element = ElementTree.SubElement(cpnet, "page", id=page.id)
|
|
286
|
+
ElementTree.SubElement(element, "pageattr", name=page.name)
|
|
287
|
+
for place in page.places:
|
|
288
|
+
_write_place(element, place)
|
|
289
|
+
for transition in page.transitions:
|
|
290
|
+
_write_transition(element, transition)
|
|
291
|
+
for order, arc in enumerate(page.arcs, start=1):
|
|
292
|
+
_write_arc(element, arc, order, page)
|
|
293
|
+
ElementTree.SubElement(element, "constraints")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
# Sections we preserve rather than model
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
_PRESERVED_SECTIONS = ("instances", "options", "binders", "monitorblock", "IndexNode")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _write_preserved(cpnet: ElementTree.Element, net: CPNet) -> None:
|
|
303
|
+
"""Copy sections we do not model from the original file, or emit defaults."""
|
|
304
|
+
original = None
|
|
305
|
+
if net.source_tree is not None:
|
|
306
|
+
original = net.source_tree.find("cpnet")
|
|
307
|
+
if original is None and net.source_tree.tag == "cpnet":
|
|
308
|
+
original = net.source_tree
|
|
309
|
+
|
|
310
|
+
for section in _PRESERVED_SECTIONS:
|
|
311
|
+
existing = original.find(section) if original is not None else None
|
|
312
|
+
if existing is not None:
|
|
313
|
+
cpnet.append(existing)
|
|
314
|
+
continue
|
|
315
|
+
if section == "instances":
|
|
316
|
+
instances = ElementTree.SubElement(cpnet, "instances")
|
|
317
|
+
for page in net.pages:
|
|
318
|
+
ElementTree.SubElement(
|
|
319
|
+
instances, "instance", id=_fresh_id(), page=page.id
|
|
320
|
+
)
|
|
321
|
+
elif section == "monitorblock":
|
|
322
|
+
ElementTree.SubElement(cpnet, "monitorblock", name="Monitors")
|
|
323
|
+
elif section in ("options", "binders"):
|
|
324
|
+
ElementTree.SubElement(cpnet, section)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# Public entry points
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
def to_xml_string(net: CPNet, pretty: bool = True) -> str:
|
|
331
|
+
"""Serialise a model to ``.cpn`` XML text (including the DOCTYPE)."""
|
|
332
|
+
from .. import __version__
|
|
333
|
+
root = ElementTree.Element("workspaceElements")
|
|
334
|
+
ElementTree.SubElement(
|
|
335
|
+
root, "generator", tool="OpenProcess", version=__version__, format="6"
|
|
336
|
+
)
|
|
337
|
+
cpnet = ElementTree.SubElement(root, "cpnet")
|
|
338
|
+
|
|
339
|
+
_write_globbox(cpnet, net)
|
|
340
|
+
for page in net.pages:
|
|
341
|
+
_write_page(cpnet, page)
|
|
342
|
+
_write_preserved(cpnet, net)
|
|
343
|
+
|
|
344
|
+
raw = ElementTree.tostring(root, encoding="unicode")
|
|
345
|
+
if pretty:
|
|
346
|
+
# minidom's pretty printer is slow but the files are small, and a
|
|
347
|
+
# readable .cpn is genuinely useful when diffing models in git.
|
|
348
|
+
parsed = minidom.parseString(raw)
|
|
349
|
+
raw = parsed.documentElement.toprettyxml(indent=" ")
|
|
350
|
+
return f'<?xml version="1.0" encoding="iso-8859-1"?>\n{DOCTYPE}\n{raw}'
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def write_cpn(net: CPNet, path: str | Path, pretty: bool = True) -> Path:
|
|
354
|
+
"""Save a model to disk and return the path written."""
|
|
355
|
+
file_path = Path(path)
|
|
356
|
+
file_path.write_text(to_xml_string(net, pretty=pretty), encoding="utf-8")
|
|
357
|
+
return file_path
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""OpenProcess Learn: exercise packs, their answer boxes, and checking them.
|
|
2
|
+
|
|
3
|
+
* :mod:`.sheet` -- reading ``question.md``: Markdown with ``answer`` blocks
|
|
4
|
+
and front matter;
|
|
5
|
+
* :mod:`.pack` -- exercise folders, packs of them, the student's progress,
|
|
6
|
+
points and exam settings;
|
|
7
|
+
* :mod:`.answers` and :mod:`.notation` -- reading typed answers: sets, pairs,
|
|
8
|
+
traces, numbers, markings, matrices, cuts, trees, alignments…;
|
|
9
|
+
* :mod:`.computed` -- the right answers worked out from the exercise's files
|
|
10
|
+
(``compute: alpha.T_I``), one documented function each;
|
|
11
|
+
* :mod:`.checks` -- checking an answer, with partial credit;
|
|
12
|
+
* :mod:`.exam` -- exam mode, the clock, marks and variants;
|
|
13
|
+
* :mod:`.importer` -- a past exam's text into a skeleton pack.
|
|
14
|
+
|
|
15
|
+
No Qt here: the same code checks answers in the app (``openprocess.gui.learn``)
|
|
16
|
+
and in ``openprocess exercises check`` (see ``openprocess/learn/exercise-packs.md``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .pack import Exercise, ExerciseFiles, Pack, exercise_files, load_pack, pack_root
|
|
20
|
+
from .sheet import Sheet, SheetError, Task, parse_sheet
|
|
21
|
+
|
|
22
|
+
__all__ = ["Exercise", "ExerciseFiles", "Pack", "Sheet", "SheetError", "Task",
|
|
23
|
+
"exercise_files", "load_pack", "pack_root", "parse_sheet"]
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Reading what a student typed: sets, pairs, traces, numbers.
|
|
2
|
+
|
|
3
|
+
Answers are typed the way they are written on paper, and read leniently:
|
|
4
|
+
|
|
5
|
+
========================== =========================================
|
|
6
|
+
typed read as
|
|
7
|
+
========================== =========================================
|
|
8
|
+
``a, b, c`` the set {a, b, c}
|
|
9
|
+
``{a, b}`` the set {a, b} (the braces are optional)
|
|
10
|
+
``{s1,s3}, {s2,s3}`` a set of two sets
|
|
11
|
+
``({a},{b,d}), ({b},{e})`` a set of two pairs (as in X_L and Y_L)
|
|
12
|
+
``<a, b, c>`` or ``⟨a,b⟩`` a sequence (a trace)
|
|
13
|
+
``∅`` or ``{}`` the empty set
|
|
14
|
+
========================== =========================================
|
|
15
|
+
|
|
16
|
+
Names are compared without regard to case or spacing, and ``s_1``,
|
|
17
|
+
``s_{1}`` and ``s1`` are the same name (subscripts as typed in LaTeX). The
|
|
18
|
+
same reading is applied to the expected answer, so a professor can write it
|
|
19
|
+
in any of these forms too.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Sequence(tuple):
|
|
28
|
+
"""``<a, b, c>``: kept apart from a pair ``(a, b)``."""
|
|
29
|
+
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
return "Sequence" + super().__repr__()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_SUBSCRIPT = re.compile(r"_\{([^{}]*)\}|_(\w)")
|
|
35
|
+
_LATEX = {"\\{": "{", "\\}": "}", "\\langle": "⟨", "\\rangle": "⟩", "\\emptyset": "∅",
|
|
36
|
+
"\\varnothing": "∅", "\\;": " ", "\\,": " ", "\\ ": " "}
|
|
37
|
+
_OPEN = {"{": "}", "(": ")", "<": ">", "⟨": "⟩", "[": "]"}
|
|
38
|
+
_CLOSE = set(_OPEN.values())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def name(text: str) -> str:
|
|
42
|
+
"""One name, as compared: ``" S_1 "`` and ``"s1"`` are the same."""
|
|
43
|
+
text = _SUBSCRIPT.sub(lambda m: m.group(1) if m.group(1) is not None else m.group(2), text)
|
|
44
|
+
return " ".join(text.replace("$", "").split()).casefold()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _tokens(text: str) -> list[str]:
|
|
48
|
+
# s_{1} first: its braces are not a set.
|
|
49
|
+
text = _SUBSCRIPT.sub(lambda m: m.group(1) if m.group(1) is not None else m.group(2), text)
|
|
50
|
+
for latex, plain in _LATEX.items():
|
|
51
|
+
text = text.replace(latex, plain)
|
|
52
|
+
text = text.replace("$", "")
|
|
53
|
+
tokens, current = [], []
|
|
54
|
+
for char in text:
|
|
55
|
+
if char in _OPEN or char in _CLOSE or char in ",;":
|
|
56
|
+
if "".join(current).strip():
|
|
57
|
+
tokens.append("".join(current).strip())
|
|
58
|
+
current = []
|
|
59
|
+
tokens.append("," if char == ";" else char)
|
|
60
|
+
else:
|
|
61
|
+
current.append(char)
|
|
62
|
+
if "".join(current).strip():
|
|
63
|
+
tokens.append("".join(current).strip())
|
|
64
|
+
return tokens
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AnswerSyntaxError(ValueError):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _parse_items(tokens: list[str], position: int, closing: str | None):
|
|
72
|
+
items, expecting_item = [], True
|
|
73
|
+
while position < len(tokens):
|
|
74
|
+
token = tokens[position]
|
|
75
|
+
if token == closing:
|
|
76
|
+
return items, position + 1
|
|
77
|
+
if token in _CLOSE:
|
|
78
|
+
raise AnswerSyntaxError(f"“{token}” does not close anything here")
|
|
79
|
+
if token == ",":
|
|
80
|
+
expecting_item = True
|
|
81
|
+
position += 1
|
|
82
|
+
continue
|
|
83
|
+
if not expecting_item:
|
|
84
|
+
raise AnswerSyntaxError("a comma is missing between two items")
|
|
85
|
+
if token in _OPEN:
|
|
86
|
+
inner, position = _parse_items(tokens, position + 1, _OPEN[token])
|
|
87
|
+
if token == "{":
|
|
88
|
+
item = frozenset(inner)
|
|
89
|
+
elif token in ("<", "⟨"):
|
|
90
|
+
item = Sequence(inner)
|
|
91
|
+
elif token == "[":
|
|
92
|
+
item = tuple(inner) # a multiset/list: order kept
|
|
93
|
+
else:
|
|
94
|
+
item = tuple(inner)
|
|
95
|
+
items.append(item)
|
|
96
|
+
elif token == "∅":
|
|
97
|
+
items.append(frozenset())
|
|
98
|
+
position += 1
|
|
99
|
+
else:
|
|
100
|
+
items.append(name(token))
|
|
101
|
+
position += 1
|
|
102
|
+
expecting_item = False
|
|
103
|
+
if closing is not None:
|
|
104
|
+
raise AnswerSyntaxError(f"“{closing}” is missing")
|
|
105
|
+
return items, position
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse(text: str) -> list:
|
|
109
|
+
"""The top-level items of an answer (each a name, set, pair or sequence)."""
|
|
110
|
+
items, _ = _parse_items(_tokens(text), 0, None)
|
|
111
|
+
return items
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def readings(text: str) -> list:
|
|
115
|
+
"""The ways ``text`` can be read as a set: the items as a set, and, when it
|
|
116
|
+
is one braced set, that set itself (``{a, b}`` and ``a, b`` agree, and
|
|
117
|
+
``{{s1}}`` is the same as ``{s1}`` written with outer braces)."""
|
|
118
|
+
items = parse(text)
|
|
119
|
+
result = [frozenset(items)]
|
|
120
|
+
if len(items) == 1 and isinstance(items[0], frozenset):
|
|
121
|
+
result.append(items[0])
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def as_set(value) -> frozenset:
|
|
126
|
+
"""An expected value (from a compute or a parsed answer) as a frozenset."""
|
|
127
|
+
if isinstance(value, str):
|
|
128
|
+
options = readings(value)
|
|
129
|
+
return options[-1] if len(options) > 1 else options[0]
|
|
130
|
+
return frozenset(normalised(v) for v in value)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def normalised(value):
|
|
134
|
+
"""Names inside a computed value, compared as typed answers are."""
|
|
135
|
+
if isinstance(value, str):
|
|
136
|
+
return name(value)
|
|
137
|
+
if isinstance(value, frozenset | set):
|
|
138
|
+
return frozenset(normalised(v) for v in value)
|
|
139
|
+
if isinstance(value, Sequence):
|
|
140
|
+
return Sequence(normalised(v) for v in value)
|
|
141
|
+
if isinstance(value, tuple | list):
|
|
142
|
+
return tuple(normalised(v) for v in value)
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def show(value) -> str:
|
|
147
|
+
"""A value written back in the notation of the course."""
|
|
148
|
+
if isinstance(value, str):
|
|
149
|
+
return value
|
|
150
|
+
if isinstance(value, frozenset | set):
|
|
151
|
+
if not value:
|
|
152
|
+
return "∅"
|
|
153
|
+
return "{" + ", ".join(sorted(show(v) for v in value)) + "}"
|
|
154
|
+
if isinstance(value, Sequence):
|
|
155
|
+
return "⟨" + ", ".join(show(v) for v in value) + "⟩"
|
|
156
|
+
if isinstance(value, tuple | list):
|
|
157
|
+
return "(" + ", ".join(show(v) for v in value) + ")"
|
|
158
|
+
return str(value)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def show_set(value) -> str:
|
|
162
|
+
"""A set of items without its outer braces when they are themselves sets or
|
|
163
|
+
pairs: ``{a}, {b}`` rather than ``{{a}, {b}}``."""
|
|
164
|
+
if isinstance(value, frozenset | set) and value and all(
|
|
165
|
+
isinstance(v, frozenset | tuple) for v in value):
|
|
166
|
+
return ", ".join(sorted(show(v) for v in value))
|
|
167
|
+
return show(value)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def number(text: str) -> float:
|
|
171
|
+
"""``0.75``, ``0,75``, ``3/4`` or ``75%``."""
|
|
172
|
+
text = text.strip().replace(" ", "")
|
|
173
|
+
if text.endswith("%"):
|
|
174
|
+
return float(text[:-1].replace(",", ".")) / 100
|
|
175
|
+
if "/" in text:
|
|
176
|
+
top, bottom = text.split("/", 1)
|
|
177
|
+
return float(top.replace(",", ".")) / float(bottom.replace(",", "."))
|
|
178
|
+
return float(text.replace(",", "."))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def trace(text: str) -> list[str]:
|
|
182
|
+
"""A firing sequence: ``register, send letter`` or ``⟨register, send letter⟩``."""
|
|
183
|
+
items = parse(text)
|
|
184
|
+
if len(items) == 1 and isinstance(items[0], Sequence | tuple):
|
|
185
|
+
items = list(items[0])
|
|
186
|
+
if not all(isinstance(item, str) for item in items):
|
|
187
|
+
raise AnswerSyntaxError("write the steps as names separated by commas")
|
|
188
|
+
return items
|