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,389 @@
|
|
|
1
|
+
"""Reader for CPN Tools ``.cpn`` files.
|
|
2
|
+
|
|
3
|
+
File shape
|
|
4
|
+
----------
|
|
5
|
+
A ``.cpn`` file is XML with this skeleton (attributes elided)::
|
|
6
|
+
|
|
7
|
+
<workspaceElements>
|
|
8
|
+
<generator tool="CPN Tools" version="4.0.1" format="6"/>
|
|
9
|
+
<cpnet>
|
|
10
|
+
<globbox> <!-- declarations, grouped into blocks -->
|
|
11
|
+
<block><id>...</id>
|
|
12
|
+
<color id="..."><id>E</id>... <layout>colset E = ...;</layout></color>
|
|
13
|
+
<var id="..."><type><id>E</id></type><id>c</id><layout>var c : E;</layout></var>
|
|
14
|
+
<ml id="...">fun f x = ...<layout>fun f x = ...</layout></ml>
|
|
15
|
+
</block>
|
|
16
|
+
</globbox>
|
|
17
|
+
<page id="...">
|
|
18
|
+
<pageattr name="Top"/>
|
|
19
|
+
<place id="..."> ... </place>
|
|
20
|
+
<trans id="..."> ... </trans>
|
|
21
|
+
<arc id="..." orientation="PtoT"> ... </arc>
|
|
22
|
+
</page>
|
|
23
|
+
<instances/> <options/> <binders/> <monitorblock/>
|
|
24
|
+
</cpnet>
|
|
25
|
+
</workspaceElements>
|
|
26
|
+
|
|
27
|
+
Robustness strategy
|
|
28
|
+
-------------------
|
|
29
|
+
CPN Tools has emitted several format revisions and third-party tools emit
|
|
30
|
+
near-misses, so the reader is deliberately forgiving:
|
|
31
|
+
|
|
32
|
+
* unknown elements and attributes are ignored, not rejected;
|
|
33
|
+
* every field has a sensible default, so a missing ``<lineattr>`` costs you a
|
|
34
|
+
line colour, not the whole file;
|
|
35
|
+
* declaration text is taken from the ``<layout>`` element whenever present,
|
|
36
|
+
because that is the exact source the modeller typed. Only if it is missing
|
|
37
|
+
do we reconstruct the declaration from the structured sub-elements.
|
|
38
|
+
|
|
39
|
+
The DTD is referenced in the file's DOCTYPE but is not fetched: parsing runs
|
|
40
|
+
with entity resolution off, so a ``.cpn`` file cannot make us open a network
|
|
41
|
+
connection or read a local file.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import xml.etree.ElementTree as ElementTree
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from ..model.declarations import parse_variable_declaration
|
|
51
|
+
from ..model.net import Arc, CPNet, Graphics, Page, Place, Transition
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Small helpers for pulling values out of the tree
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
def _text_of(element: ElementTree.Element | None, default: str = "") -> str:
|
|
58
|
+
"""Text content of an element, with whitespace trimmed."""
|
|
59
|
+
if element is None or element.text is None:
|
|
60
|
+
return default
|
|
61
|
+
return element.text.strip()
|
|
62
|
+
|
|
63
|
+
def _child_text(parent: ElementTree.Element, tag: str, default: str = "") -> str:
|
|
64
|
+
"""Text of ``parent/tag/text`` -- the shape CPN Tools uses for inscriptions."""
|
|
65
|
+
child = parent.find(tag)
|
|
66
|
+
if child is None:
|
|
67
|
+
return default
|
|
68
|
+
inner = child.find("text")
|
|
69
|
+
if inner is not None:
|
|
70
|
+
return _text_of(inner, default)
|
|
71
|
+
return _text_of(child, default)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _float_attribute(element: ElementTree.Element | None, name: str, default: float) -> float:
|
|
75
|
+
if element is None:
|
|
76
|
+
return default
|
|
77
|
+
raw = element.get(name)
|
|
78
|
+
if raw is None:
|
|
79
|
+
return default
|
|
80
|
+
try:
|
|
81
|
+
return float(raw)
|
|
82
|
+
except ValueError:
|
|
83
|
+
return default
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _read_graphics(element: ElementTree.Element,
|
|
87
|
+
shape_tag: str | None = None) -> Graphics:
|
|
88
|
+
"""Read the ``posattr`` / ``fillattr`` / ``lineattr`` / ``textattr`` group.
|
|
89
|
+
|
|
90
|
+
``shape_tag`` is ``"ellipse"`` for places and ``"box"`` for transitions;
|
|
91
|
+
both carry the width and height as ``w`` and ``h``.
|
|
92
|
+
"""
|
|
93
|
+
graphics = Graphics()
|
|
94
|
+
position = element.find("posattr")
|
|
95
|
+
graphics.x = _float_attribute(position, "x", 0.0)
|
|
96
|
+
graphics.y = _float_attribute(position, "y", 0.0)
|
|
97
|
+
|
|
98
|
+
fill = element.find("fillattr")
|
|
99
|
+
if fill is not None:
|
|
100
|
+
graphics.fill_colour = fill.get("colour", graphics.fill_colour)
|
|
101
|
+
|
|
102
|
+
line = element.find("lineattr")
|
|
103
|
+
if line is not None:
|
|
104
|
+
graphics.line_colour = line.get("colour", graphics.line_colour)
|
|
105
|
+
try:
|
|
106
|
+
graphics.line_width = float(line.get("thick", graphics.line_width))
|
|
107
|
+
except ValueError:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
text = element.find("textattr")
|
|
111
|
+
if text is not None:
|
|
112
|
+
graphics.text_colour = text.get("colour", graphics.text_colour)
|
|
113
|
+
|
|
114
|
+
if shape_tag:
|
|
115
|
+
shape = element.find(shape_tag)
|
|
116
|
+
graphics.width = _float_attribute(shape, "w", graphics.width)
|
|
117
|
+
graphics.height = _float_attribute(shape, "h", graphics.height)
|
|
118
|
+
|
|
119
|
+
# Where the modeller put each inscription (the centre of its text), kept
|
|
120
|
+
# as an offset from the element so the label moves with it. Arcs have no
|
|
121
|
+
# position of their own: their offsets are absolute and the canvas turns
|
|
122
|
+
# them into offsets from the arc's midpoint.
|
|
123
|
+
for tag in ("type", "initmark", "cond", "time", "annot"):
|
|
124
|
+
region = element.find(tag)
|
|
125
|
+
if region is None:
|
|
126
|
+
continue
|
|
127
|
+
region_position = region.find("posattr")
|
|
128
|
+
if region_position is None:
|
|
129
|
+
continue
|
|
130
|
+
x = _float_attribute(region_position, "x", 0.0)
|
|
131
|
+
y = _float_attribute(region_position, "y", 0.0)
|
|
132
|
+
if shape_tag:
|
|
133
|
+
graphics.label_offsets[tag] = (x - graphics.x, y - graphics.y)
|
|
134
|
+
else:
|
|
135
|
+
graphics.label_offsets[tag] = (x, y)
|
|
136
|
+
|
|
137
|
+
# The current-marking bubble (``token``) and text (``marking``) of a place.
|
|
138
|
+
for tag in ("token", "marking"):
|
|
139
|
+
region = element.find(tag)
|
|
140
|
+
if region is not None:
|
|
141
|
+
graphics.label_offsets[tag] = (_float_attribute(region, "x", 0.0),
|
|
142
|
+
_float_attribute(region, "y", 0.0))
|
|
143
|
+
marking = element.find("marking")
|
|
144
|
+
if marking is not None:
|
|
145
|
+
graphics.marking_hidden = marking.get("hidden", "false").strip().lower() == "true"
|
|
146
|
+
|
|
147
|
+
return graphics
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Declarations
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
def _reconstruct_colour_set_declaration(element: ElementTree.Element) -> str:
|
|
154
|
+
"""Rebuild ``colset NAME = ...;`` from the structured XML.
|
|
155
|
+
|
|
156
|
+
Only used when the ``<layout>`` element is absent. We cover the common
|
|
157
|
+
forms; anything exotic falls back to an alias of ``UNIT`` so that the model
|
|
158
|
+
still loads and the problem shows up as a compile issue rather than a
|
|
159
|
+
crash.
|
|
160
|
+
"""
|
|
161
|
+
name = _text_of(element.find("id"), "UNNAMED")
|
|
162
|
+
timed = " timed" if element.find("timed") is not None else ""
|
|
163
|
+
|
|
164
|
+
if element.find("enum") is not None:
|
|
165
|
+
constants = [_text_of(i) for i in element.find("enum").findall("id")]
|
|
166
|
+
return f"colset {name} = with {' | '.join(constants)}{timed};"
|
|
167
|
+
|
|
168
|
+
if element.find("product") is not None:
|
|
169
|
+
parts = [_text_of(i) for i in element.find("product").findall("id")]
|
|
170
|
+
return f"colset {name} = product {' * '.join(parts)}{timed};"
|
|
171
|
+
|
|
172
|
+
if element.find("record") is not None:
|
|
173
|
+
fields = []
|
|
174
|
+
for record_field in element.find("record").findall("recordfield"):
|
|
175
|
+
fields.append(
|
|
176
|
+
f"{_text_of(record_field.find('id'))}:{_text_of(record_field.find('id'), '')}"
|
|
177
|
+
)
|
|
178
|
+
return f"colset {name} = record {' * '.join(fields)}{timed};"
|
|
179
|
+
|
|
180
|
+
if element.find("list") is not None:
|
|
181
|
+
inner = _text_of(element.find("list").find("id"), "UNIT")
|
|
182
|
+
return f"colset {name} = list {inner}{timed};"
|
|
183
|
+
|
|
184
|
+
if element.find("index") is not None:
|
|
185
|
+
index = element.find("index")
|
|
186
|
+
tag = _text_of(index.find("id"), "idx")
|
|
187
|
+
bounds = [_text_of(m) for m in index.findall("ml")]
|
|
188
|
+
low, high = (bounds + ["1", "1"])[:2]
|
|
189
|
+
return f"colset {name} = index {tag} with {low}..{high}{timed};"
|
|
190
|
+
|
|
191
|
+
if element.find("union") is not None:
|
|
192
|
+
variants = []
|
|
193
|
+
for union_field in element.find("union").findall("unionfield"):
|
|
194
|
+
tag = _text_of(union_field.find("id"))
|
|
195
|
+
payload = union_field.find("id[2]")
|
|
196
|
+
variants.append(f"{tag}:{_text_of(payload)}" if payload is not None else tag)
|
|
197
|
+
return f"colset {name} = union {' + '.join(variants)}{timed};"
|
|
198
|
+
|
|
199
|
+
for simple in ("unit", "bool", "int", "intinf", "real", "string"):
|
|
200
|
+
if element.find(simple) is not None:
|
|
201
|
+
return f"colset {name} = {simple}{timed};"
|
|
202
|
+
|
|
203
|
+
# `alias` or anything unrecognised.
|
|
204
|
+
alias = element.find("alias")
|
|
205
|
+
if alias is not None:
|
|
206
|
+
return f"colset {name} = {_text_of(alias.find('id'), 'UNIT')}{timed};"
|
|
207
|
+
return f"colset {name} = UNIT{timed};"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _read_globbox(globbox: ElementTree.Element, net: CPNet) -> None:
|
|
211
|
+
"""Walk the declaration tree, in document order, filling the model.
|
|
212
|
+
|
|
213
|
+
Blocks may nest, so this recurses. Order is preserved because a colour set
|
|
214
|
+
may refer to earlier ones.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def visit(node: ElementTree.Element) -> None:
|
|
218
|
+
for child in node:
|
|
219
|
+
tag = child.tag
|
|
220
|
+
if tag == "block":
|
|
221
|
+
visit(child)
|
|
222
|
+
elif tag == "color":
|
|
223
|
+
layout = _text_of(child.find("layout"))
|
|
224
|
+
source = layout or _reconstruct_colour_set_declaration(child)
|
|
225
|
+
name = _text_of(child.find("id"), "UNNAMED")
|
|
226
|
+
net.declarations.colour_set_sources.append((name, source))
|
|
227
|
+
elif tag == "var":
|
|
228
|
+
layout = _text_of(child.find("layout"))
|
|
229
|
+
if layout:
|
|
230
|
+
try:
|
|
231
|
+
for declaration in parse_variable_declaration(layout):
|
|
232
|
+
net.declarations.variable_sources.append(
|
|
233
|
+
(declaration.name, declaration.colour_set_name, layout)
|
|
234
|
+
)
|
|
235
|
+
continue
|
|
236
|
+
except Exception:
|
|
237
|
+
pass # fall through to the structured form
|
|
238
|
+
colour_set_name = _text_of(child.find("type/id"), "UNIT")
|
|
239
|
+
for identifier in child.findall("id"):
|
|
240
|
+
variable_name = _text_of(identifier)
|
|
241
|
+
if variable_name:
|
|
242
|
+
net.declarations.variable_sources.append(
|
|
243
|
+
(variable_name, colour_set_name,
|
|
244
|
+
f"var {variable_name} : {colour_set_name};")
|
|
245
|
+
)
|
|
246
|
+
elif tag == "ml":
|
|
247
|
+
layout = _text_of(child.find("layout"))
|
|
248
|
+
source = layout or _text_of(child)
|
|
249
|
+
if source:
|
|
250
|
+
net.declarations.ml_sources.append(source)
|
|
251
|
+
elif tag == "globref":
|
|
252
|
+
name = _text_of(child.find("id"), "")
|
|
253
|
+
initial = _text_of(child.find("ml"), "0")
|
|
254
|
+
if name:
|
|
255
|
+
net.declarations.globref_sources.append((name, initial))
|
|
256
|
+
|
|
257
|
+
visit(globbox)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Net elements
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
def _read_place(element: ElementTree.Element) -> Place:
|
|
264
|
+
place = Place(
|
|
265
|
+
id=element.get("id", ""),
|
|
266
|
+
name=_text_of(element.find("text")),
|
|
267
|
+
colour_set_name=_child_text(element, "type", "UNIT"),
|
|
268
|
+
initial_marking_text=_child_text(element, "initmark", ""),
|
|
269
|
+
graphics=_read_graphics(element, "ellipse"),
|
|
270
|
+
source_element=element,
|
|
271
|
+
)
|
|
272
|
+
port = element.find("port")
|
|
273
|
+
if port is not None:
|
|
274
|
+
place.port_type = port.get("type", "General")
|
|
275
|
+
fusion = element.find("fusioninfo")
|
|
276
|
+
if fusion is not None:
|
|
277
|
+
place.fusion_group = fusion.get("name")
|
|
278
|
+
return place
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _read_transition(element: ElementTree.Element) -> Transition:
|
|
282
|
+
transition = Transition(
|
|
283
|
+
id=element.get("id", ""),
|
|
284
|
+
name=_text_of(element.find("text")),
|
|
285
|
+
guard_text=_child_text(element, "cond", ""),
|
|
286
|
+
time_text=_child_text(element, "time", ""),
|
|
287
|
+
code_text=_child_text(element, "code", ""),
|
|
288
|
+
priority_text=_child_text(element, "priority", ""),
|
|
289
|
+
graphics=_read_graphics(element, "box"),
|
|
290
|
+
source_element=element,
|
|
291
|
+
)
|
|
292
|
+
substitution = element.find("subst")
|
|
293
|
+
if substitution is not None:
|
|
294
|
+
transition.substitution_subpage = substitution.get("subpage")
|
|
295
|
+
# `portsock` is a flat string of alternating socket/port ids in
|
|
296
|
+
# parentheses, e.g. "(ID1,ID2)(ID3,ID4)".
|
|
297
|
+
raw = substitution.get("portsock", "")
|
|
298
|
+
for pair in raw.replace(")", ")\n").split():
|
|
299
|
+
cleaned = pair.strip("()")
|
|
300
|
+
if "," in cleaned:
|
|
301
|
+
socket, port = cleaned.split(",", 1)
|
|
302
|
+
transition.port_assignments[socket.strip()] = port.strip()
|
|
303
|
+
return transition
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _read_arc(element: ElementTree.Element) -> Arc:
|
|
307
|
+
place_end = element.find("placeend")
|
|
308
|
+
transition_end = element.find("transend")
|
|
309
|
+
arc = Arc(
|
|
310
|
+
id=element.get("id", ""),
|
|
311
|
+
place_id=place_end.get("idref", "") if place_end is not None else "",
|
|
312
|
+
transition_id=transition_end.get("idref", "") if transition_end is not None else "",
|
|
313
|
+
orientation=element.get("orientation", "PtoT"),
|
|
314
|
+
expression_text=_child_text(element, "annot", ""),
|
|
315
|
+
graphics=_read_graphics(element),
|
|
316
|
+
source_element=element,
|
|
317
|
+
)
|
|
318
|
+
for bendpoint in element.findall("bendpoint"):
|
|
319
|
+
position = bendpoint.find("posattr")
|
|
320
|
+
arc.bendpoints.append(
|
|
321
|
+
(_float_attribute(position, "x", 0.0), _float_attribute(position, "y", 0.0))
|
|
322
|
+
)
|
|
323
|
+
return arc
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _read_page(element: ElementTree.Element) -> Page:
|
|
327
|
+
attributes = element.find("pageattr")
|
|
328
|
+
page = Page(
|
|
329
|
+
id=element.get("id", ""),
|
|
330
|
+
name=attributes.get("name", "Page") if attributes is not None else "Page",
|
|
331
|
+
source_element=element,
|
|
332
|
+
)
|
|
333
|
+
page.places = [_read_place(e) for e in element.findall("place")]
|
|
334
|
+
page.transitions = [_read_transition(e) for e in element.findall("trans")]
|
|
335
|
+
arcs = [_read_arc(e) for e in element.findall("arc")]
|
|
336
|
+
# Files edited in CPN Tools sometimes keep arcs whose place or transition
|
|
337
|
+
# was deleted (the course's PlaneBoardingExample.cpn has two). CPN Tools
|
|
338
|
+
# ignores them; so do we -- an arc to nowhere cannot be simulated or drawn.
|
|
339
|
+
place_ids = {p.id for p in page.places}
|
|
340
|
+
transition_ids = {t.id for t in page.transitions}
|
|
341
|
+
page.arcs = [a for a in arcs if a.place_id in place_ids and a.transition_id in transition_ids]
|
|
342
|
+
page.dropped_arcs = len(arcs) - len(page.arcs)
|
|
343
|
+
return page
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
# Public entry points
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
def parse_cpn(xml_text: str, name: str = "Untitled") -> CPNet:
|
|
350
|
+
"""Parse ``.cpn`` XML held in a string."""
|
|
351
|
+
# ``XMLParser`` with the default target does not expand external entities,
|
|
352
|
+
# so a malicious DOCTYPE cannot read local files or hit the network.
|
|
353
|
+
root = ElementTree.fromstring(xml_text)
|
|
354
|
+
|
|
355
|
+
net = CPNet(name=name)
|
|
356
|
+
net.source_tree = root
|
|
357
|
+
|
|
358
|
+
cpnet = root.find("cpnet") if root.tag != "cpnet" else root
|
|
359
|
+
if cpnet is None:
|
|
360
|
+
raise ValueError("not a CPN Tools file: no <cpnet> element found")
|
|
361
|
+
|
|
362
|
+
globbox = cpnet.find("globbox")
|
|
363
|
+
if globbox is not None:
|
|
364
|
+
_read_globbox(globbox, net)
|
|
365
|
+
|
|
366
|
+
for page_element in cpnet.findall("page"):
|
|
367
|
+
net.pages.append(_read_page(page_element))
|
|
368
|
+
|
|
369
|
+
# Fusion sets are listed separately; each names the places it unifies.
|
|
370
|
+
for fusion in cpnet.findall(".//fusion"):
|
|
371
|
+
group = fusion.get("name", "")
|
|
372
|
+
members = [e.get("idref", "") for e in fusion.findall("fusion_elm")]
|
|
373
|
+
if group:
|
|
374
|
+
net.fusion_sets[group] = members
|
|
375
|
+
|
|
376
|
+
net.compile()
|
|
377
|
+
return net
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def read_cpn(path: str | Path) -> CPNet:
|
|
381
|
+
"""Load a ``.cpn`` file from disk.
|
|
382
|
+
|
|
383
|
+
The model's name is taken from the file stem, which is what CPN Tools shows
|
|
384
|
+
in its title bar.
|
|
385
|
+
"""
|
|
386
|
+
file_path = Path(path)
|
|
387
|
+
net = parse_cpn(file_path.read_text(encoding="utf-8", errors="replace"),
|
|
388
|
+
name=file_path.stem)
|
|
389
|
+
return net
|