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,374 @@
|
|
|
1
|
+
"""Structural properties of WF-nets (van der Aalst, *Workflow Verification*, §3 and §6).
|
|
2
|
+
|
|
3
|
+
These look only at the drawing -- places, transitions, arcs -- never at
|
|
4
|
+
markings, so they are quick and they point at the *construct* that causes a
|
|
5
|
+
problem instead of at a firing sequence.
|
|
6
|
+
|
|
7
|
+
Free-choice (Definition 7)
|
|
8
|
+
Whenever two transitions share an input place, they have exactly the
|
|
9
|
+
same input places: ``•t1 ∩ •t2 ≠ ∅ ⇒ •t1 = •t2``. Every choice is then
|
|
10
|
+
a "free" one, not influenced by what happened in parallel branches.
|
|
11
|
+
(Corollary 1: soundness of a free-choice WF-net can be decided in
|
|
12
|
+
polynomial time; Lemma 1: a sound free-choice WF-net is safe.)
|
|
13
|
+
|
|
14
|
+
Well-handled (Definition 13) and well-structured (Definition 14)
|
|
15
|
+
A net is well-handled when there is no place/transition pair ``x, y``
|
|
16
|
+
with two *different* elementary paths from ``x`` to ``y`` that share
|
|
17
|
+
only ``x`` and ``y``. Such a pair is a **handle**:
|
|
18
|
+
|
|
19
|
+
* PT-handle -- ``x`` is a place, ``y`` a transition: a choice (OR-split)
|
|
20
|
+
that is later synchronised (AND-join).
|
|
21
|
+
* TP-handle -- ``x`` is a transition, ``y`` a place: parallel branches
|
|
22
|
+
(AND-split) that are later merged as alternatives (OR-join).
|
|
23
|
+
|
|
24
|
+
A WF-net is well-structured iff its short-circuited net ``N̄`` is
|
|
25
|
+
well-handled (the extra transition ``t*`` catches handles that go
|
|
26
|
+
"around" the net). Corollary 2 / Lemma 3: soundness is polynomial and a
|
|
27
|
+
sound well-structured WF-net is safe.
|
|
28
|
+
|
|
29
|
+
Two paths sharing only their ends are two *internally vertex-disjoint*
|
|
30
|
+
paths, so (Menger's theorem) a handle exists iff the maximum number of
|
|
31
|
+
such paths from ``x`` to ``y`` is at least 2 -- a small max-flow problem
|
|
32
|
+
with every inner node given capacity 1.
|
|
33
|
+
|
|
34
|
+
S-coverable (Definitions 8--10 and 16)
|
|
35
|
+
An S-component is a strongly connected subnet that is a *state
|
|
36
|
+
machine* (each transition has one input and one output place inside it)
|
|
37
|
+
and takes along every arc of its places. Think of it as one "document"
|
|
38
|
+
or thread that is always in exactly one place. A WF-net is S-coverable
|
|
39
|
+
when every node of ``N̄`` lies in some S-component. Sound free-choice
|
|
40
|
+
and sound well-structured WF-nets are S-coverable (Corollaries 3, 4),
|
|
41
|
+
and an S-coverable WF-net is safe.
|
|
42
|
+
|
|
43
|
+
Lemma 4 (a necessary condition for soundness)
|
|
44
|
+
In a sound WF-net a transition that consumes from ``i`` consumes *only*
|
|
45
|
+
from ``i``, and a transition that produces into ``o`` produces *only*
|
|
46
|
+
into ``o`` -- otherwise it needs ``i`` (or ``o``) together with another
|
|
47
|
+
marked place, which never happens.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from collections import deque
|
|
53
|
+
from dataclasses import dataclass, field
|
|
54
|
+
|
|
55
|
+
from .petrinet import PetriNet
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Free-choice
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
def free_choice_violations(net: PetriNet) -> list[tuple[str, str, str]]:
|
|
62
|
+
"""``(t1, t2, shared place)`` for every pair that breaks Definition 7."""
|
|
63
|
+
violations = []
|
|
64
|
+
transitions = list(net.transitions)
|
|
65
|
+
for index, t1 in enumerate(transitions):
|
|
66
|
+
inputs1 = net.preset(t1)
|
|
67
|
+
for t2 in transitions[index + 1:]:
|
|
68
|
+
inputs2 = net.preset(t2)
|
|
69
|
+
shared = inputs1 & inputs2
|
|
70
|
+
if shared and inputs1 != inputs2:
|
|
71
|
+
violations.append((t1, t2, sorted(shared)[0]))
|
|
72
|
+
return violations
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Handles (well-handledness)
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
@dataclass
|
|
79
|
+
class Handle:
|
|
80
|
+
"""Two elementary paths from ``start`` to ``end`` sharing only the ends."""
|
|
81
|
+
|
|
82
|
+
start: str
|
|
83
|
+
end: str
|
|
84
|
+
paths: tuple[list[str], list[str]]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def kind(self) -> str:
|
|
88
|
+
return "PT" if self.start.startswith("p:") else "TP"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _two_disjoint_paths(successors: dict[str, list[str]], start: str, end: str):
|
|
92
|
+
"""Two internally vertex-disjoint paths ``start → end``, or None.
|
|
93
|
+
|
|
94
|
+
Max-flow with vertex splitting: node ``v`` becomes ``(v, 0) → (v, 1)``
|
|
95
|
+
with capacity 1 (unlimited for ``start``/``end``); an arc ``u → v`` is
|
|
96
|
+
``(u, 1) → (v, 0)`` with capacity 1. Two augmenting paths = two paths.
|
|
97
|
+
"""
|
|
98
|
+
capacity: dict[tuple, dict[tuple, int]] = {}
|
|
99
|
+
|
|
100
|
+
def add(u, v, c):
|
|
101
|
+
capacity.setdefault(u, {})[v] = capacity.get(u, {}).get(v, 0) + c
|
|
102
|
+
capacity.setdefault(v, {}).setdefault(u, 0)
|
|
103
|
+
|
|
104
|
+
for node, targets in successors.items():
|
|
105
|
+
add((node, 0), (node, 1), 2 if node in (start, end) else 1)
|
|
106
|
+
for target in targets:
|
|
107
|
+
add((node, 1), (target, 0), 1)
|
|
108
|
+
source, sink = (start, 1), (end, 0)
|
|
109
|
+
flow_edges: dict[tuple, dict[tuple, int]] = {u: dict(v) for u, v in capacity.items()}
|
|
110
|
+
found = 0
|
|
111
|
+
for _ in range(2):
|
|
112
|
+
parent = {source: None}
|
|
113
|
+
queue = deque([source])
|
|
114
|
+
while queue and sink not in parent:
|
|
115
|
+
u = queue.popleft()
|
|
116
|
+
for v, c in flow_edges.get(u, {}).items():
|
|
117
|
+
if c > 0 and v not in parent:
|
|
118
|
+
parent[v] = u
|
|
119
|
+
queue.append(v)
|
|
120
|
+
if sink not in parent:
|
|
121
|
+
break
|
|
122
|
+
v = sink
|
|
123
|
+
while parent[v] is not None:
|
|
124
|
+
u = parent[v]
|
|
125
|
+
flow_edges[u][v] -= 1
|
|
126
|
+
flow_edges[v][u] += 1
|
|
127
|
+
v = u
|
|
128
|
+
found += 1
|
|
129
|
+
if found < 2:
|
|
130
|
+
return None
|
|
131
|
+
# Read the two paths off the flow: follow edges that carry flow.
|
|
132
|
+
carried = {u: {v: capacity[u][v] - c for v, c in targets.items()
|
|
133
|
+
if capacity[u][v] > 0 and capacity[u][v] - c > 0}
|
|
134
|
+
for u, targets in flow_edges.items()}
|
|
135
|
+
paths = []
|
|
136
|
+
for _ in range(2):
|
|
137
|
+
path, node = [start], source
|
|
138
|
+
while node != sink:
|
|
139
|
+
nxt = next(iter(carried[node]))
|
|
140
|
+
carried[node][nxt] -= 1
|
|
141
|
+
if not carried[node][nxt]:
|
|
142
|
+
del carried[node][nxt]
|
|
143
|
+
if nxt[1] == 0:
|
|
144
|
+
path.append(nxt[0])
|
|
145
|
+
node = nxt
|
|
146
|
+
paths.append(path)
|
|
147
|
+
return paths[0], paths[1]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _graph(net: PetriNet) -> dict[str, list[str]]:
|
|
151
|
+
"""Successor lists over prefixed ids (``p:…`` places, ``t:…`` transitions)."""
|
|
152
|
+
graph = {f"p:{p}": [f"t:{t}" for t in net.postset(p)] for p in net.places}
|
|
153
|
+
graph.update({f"t:{t}": [f"p:{p}" for p in net.postset(t)] for t in net.transitions})
|
|
154
|
+
return graph
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def handles(net: PetriNet, limit: int = 5) -> list[Handle]:
|
|
158
|
+
"""Up to ``limit`` handles of ``net`` (empty list: the net is well-handled).
|
|
159
|
+
|
|
160
|
+
Only splits (≥ 2 outgoing arcs) can start a handle and only joins (≥ 2
|
|
161
|
+
incoming arcs) can end one, which keeps this fast on real models.
|
|
162
|
+
"""
|
|
163
|
+
graph = _graph(net)
|
|
164
|
+
incoming: dict[str, int] = {node: 0 for node in graph}
|
|
165
|
+
for targets in graph.values():
|
|
166
|
+
for target in targets:
|
|
167
|
+
incoming[target] += 1
|
|
168
|
+
splits = [n for n, targets in graph.items() if len(targets) >= 2]
|
|
169
|
+
joins = [n for n, count in incoming.items() if count >= 2]
|
|
170
|
+
found = []
|
|
171
|
+
for start in sorted(splits):
|
|
172
|
+
for end in sorted(joins):
|
|
173
|
+
if start[0] == end[0]:
|
|
174
|
+
continue # a handle links a place and a transition
|
|
175
|
+
paths = _two_disjoint_paths(graph, start, end)
|
|
176
|
+
if paths is not None:
|
|
177
|
+
found.append(Handle(start, end, paths))
|
|
178
|
+
if len(found) >= 50:
|
|
179
|
+
break
|
|
180
|
+
# The shortest handles are the easiest to see in the drawing.
|
|
181
|
+
found.sort(key=lambda h: len(h.paths[0]) + len(h.paths[1]))
|
|
182
|
+
return found[:limit]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# S-components
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
class _Budget(Exception):
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _s_component_search(net: PetriNet, start: str, must_contain: str, budget: list[int]):
|
|
193
|
+
"""An S-component (set of place ids) containing place ``start`` and node
|
|
194
|
+
``must_contain`` (a place or transition id), or None.
|
|
195
|
+
|
|
196
|
+
Unknowns are the places; x_p = 1 means "p is in the component". For
|
|
197
|
+
every transition the state-machine rule says
|
|
198
|
+
``Σ_{p ∈ •t} x_p = Σ_{p ∈ t•} x_p ≤ 1``. We propagate that rule, branch
|
|
199
|
+
where a transition still has a choice, and check strong connectivity of
|
|
200
|
+
each solution (a solution may split into several cycles; any of them
|
|
201
|
+
that is closed is an S-component on its own).
|
|
202
|
+
"""
|
|
203
|
+
pre = {t: sorted(net.preset(t)) for t in net.transitions}
|
|
204
|
+
post = {t: sorted(net.postset(t)) for t in net.transitions}
|
|
205
|
+
touching = {p: sorted(net.preset(p) | net.postset(p)) for p in net.places}
|
|
206
|
+
|
|
207
|
+
def propagate(x: dict[str, int], changed: list[str]) -> bool:
|
|
208
|
+
queue = deque(t for p in changed for t in touching[p])
|
|
209
|
+
while queue:
|
|
210
|
+
t = queue.popleft()
|
|
211
|
+
sides = []
|
|
212
|
+
for side in (pre[t], post[t]):
|
|
213
|
+
ones = [p for p in side if x.get(p) == 1]
|
|
214
|
+
unknown = [p for p in side if p not in x]
|
|
215
|
+
sides.append((side, ones, unknown))
|
|
216
|
+
(_, ones_in, unk_in), (_, ones_out, unk_out) = sides
|
|
217
|
+
if len(ones_in) > 1 or len(ones_out) > 1:
|
|
218
|
+
return False
|
|
219
|
+
assign: list[tuple[str, int]] = []
|
|
220
|
+
for (side, ones, unknown), (_, other_ones, other_unknown) in (
|
|
221
|
+
(sides[0], sides[1]), (sides[1], sides[0])):
|
|
222
|
+
if ones:
|
|
223
|
+
assign += [(p, 0) for p in unknown]
|
|
224
|
+
if not other_ones:
|
|
225
|
+
if not other_unknown:
|
|
226
|
+
return False
|
|
227
|
+
if len(other_unknown) == 1:
|
|
228
|
+
assign.append((other_unknown[0], 1))
|
|
229
|
+
elif not unknown:
|
|
230
|
+
if other_ones:
|
|
231
|
+
return False
|
|
232
|
+
assign += [(p, 0) for p in other_unknown]
|
|
233
|
+
for place, value in assign:
|
|
234
|
+
if place in x:
|
|
235
|
+
if x[place] != value:
|
|
236
|
+
return False
|
|
237
|
+
continue
|
|
238
|
+
x[place] = value
|
|
239
|
+
queue.extend(touching[place])
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
def closed_components(places: set[str]):
|
|
243
|
+
"""The strongly connected parts of a solution that are S-components."""
|
|
244
|
+
succ = {}
|
|
245
|
+
for p in places:
|
|
246
|
+
succ[f"p:{p}"] = [f"t:{t}" for t in net.postset(p)]
|
|
247
|
+
for t in net.postset(p) | net.preset(p):
|
|
248
|
+
succ.setdefault(f"t:{t}", [f"p:{q}" for q in net.postset(t) if q in places])
|
|
249
|
+
from .analysis import strongly_connected_components
|
|
250
|
+
nodes = list(succ)
|
|
251
|
+
index = {n: i for i, n in enumerate(nodes)}
|
|
252
|
+
for component in strongly_connected_components(
|
|
253
|
+
len(nodes), lambda i: [index[m] for m in succ[nodes[i]]]):
|
|
254
|
+
members = {nodes[i] for i in component}
|
|
255
|
+
q = {n[2:] for n in members if n.startswith("p:")}
|
|
256
|
+
if not q:
|
|
257
|
+
continue
|
|
258
|
+
ts = {t for p in q for t in net.postset(p) | net.preset(p)}
|
|
259
|
+
if all(len(net.preset(t) & q) == 1 and len(net.postset(t) & q) == 1 for t in ts) \
|
|
260
|
+
and {f"t:{t}" for t in ts} <= members:
|
|
261
|
+
yield q, ts
|
|
262
|
+
|
|
263
|
+
def search(x: dict[str, int]):
|
|
264
|
+
budget[0] -= 1
|
|
265
|
+
if budget[0] < 0:
|
|
266
|
+
raise _Budget
|
|
267
|
+
for t in net.transitions:
|
|
268
|
+
for side, other in ((pre[t], post[t]), (post[t], pre[t])):
|
|
269
|
+
if any(x.get(p) == 1 for p in side) and not any(x.get(p) == 1 for p in other):
|
|
270
|
+
options = [p for p in other if p not in x]
|
|
271
|
+
for choice in options:
|
|
272
|
+
trial = dict(x)
|
|
273
|
+
trial[choice] = 1
|
|
274
|
+
for p in options:
|
|
275
|
+
if p != choice:
|
|
276
|
+
trial[p] = 0
|
|
277
|
+
if propagate(trial, options):
|
|
278
|
+
result = search(trial)
|
|
279
|
+
if result is not None:
|
|
280
|
+
return result
|
|
281
|
+
return None
|
|
282
|
+
places = {p for p, v in x.items() if v == 1}
|
|
283
|
+
for q, ts in closed_components(places):
|
|
284
|
+
if start in q and (must_contain in q or must_contain in ts):
|
|
285
|
+
return q
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
x = {start: 1}
|
|
289
|
+
if not propagate(x, [start]):
|
|
290
|
+
return None
|
|
291
|
+
return search(x)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class SCoverage:
|
|
296
|
+
components: list[set[str]] = field(default_factory=list)
|
|
297
|
+
uncovered: list[str] = field(default_factory=list) # node ids
|
|
298
|
+
complete: bool = True # False: search budget ran out
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def s_coverable(self) -> bool | None:
|
|
302
|
+
if self.uncovered and self.complete:
|
|
303
|
+
return False
|
|
304
|
+
return True if not self.uncovered else None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def s_coverage(net: PetriNet, budget: int = 20_000) -> SCoverage:
|
|
308
|
+
"""Find S-components until every node is covered (or show which is not)."""
|
|
309
|
+
result = SCoverage()
|
|
310
|
+
covered: set[str] = set()
|
|
311
|
+
remaining = [budget]
|
|
312
|
+
for node in list(net.places) + list(net.transitions):
|
|
313
|
+
if node in covered:
|
|
314
|
+
continue
|
|
315
|
+
starts = [node] if node in net.places else sorted(net.preset(node))
|
|
316
|
+
found = None
|
|
317
|
+
try:
|
|
318
|
+
for start in starts:
|
|
319
|
+
found = _s_component_search(net, start, node, remaining)
|
|
320
|
+
if found is not None:
|
|
321
|
+
break
|
|
322
|
+
except _Budget:
|
|
323
|
+
result.complete = False
|
|
324
|
+
result.uncovered.append(node)
|
|
325
|
+
continue
|
|
326
|
+
if found is None:
|
|
327
|
+
result.uncovered.append(node)
|
|
328
|
+
continue
|
|
329
|
+
result.components.append(found)
|
|
330
|
+
covered |= found
|
|
331
|
+
covered |= {t for p in found for t in net.preset(p) | net.postset(p)}
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# Everything for a WF-net
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
@dataclass
|
|
339
|
+
class StructureReport:
|
|
340
|
+
free_choice: list[tuple[str, str, str]] # violations of Definition 7
|
|
341
|
+
handles: list[Handle] | None # of N̄; None if skipped (too big)
|
|
342
|
+
coverage: SCoverage | None # of N̄
|
|
343
|
+
state_machine: bool # every transition 1 in, 1 out
|
|
344
|
+
lemma4: list[str] = field(default_factory=list) # transition ids breaking Lemma 4
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def is_free_choice(self) -> bool:
|
|
348
|
+
return not self.free_choice
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def well_structured(self) -> bool | None:
|
|
352
|
+
return None if self.handles is None else not self.handles
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def s_coverable(self) -> bool | None:
|
|
356
|
+
return None if self.coverage is None else self.coverage.s_coverable
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def check_structure(net: PetriNet, source: str, sink: str,
|
|
360
|
+
max_nodes: int = 400) -> StructureReport:
|
|
361
|
+
"""Free-choice, well-structured and S-coverable, as in §6 of the paper."""
|
|
362
|
+
from .analysis import short_circuit
|
|
363
|
+
closed = short_circuit(net, source, sink)
|
|
364
|
+
small = len(closed.places) + len(closed.transitions) <= max_nodes
|
|
365
|
+
lemma4 = [t for t in net.transitions
|
|
366
|
+
if (source in net.preset(t) and net.preset(t) != {source})
|
|
367
|
+
or (sink in net.postset(t) and net.postset(t) != {sink})]
|
|
368
|
+
return StructureReport(
|
|
369
|
+
free_choice=free_choice_violations(net),
|
|
370
|
+
handles=handles(closed) if small else None,
|
|
371
|
+
coverage=s_coverage(closed) if small else None,
|
|
372
|
+
state_machine=all(len(net.preset(t)) == 1 and len(net.postset(t)) == 1
|
|
373
|
+
for t in net.transitions),
|
|
374
|
+
lemma4=lemma4)
|