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.
Files changed (173) hide show
  1. cpnpy/__init__.py +46 -0
  2. openprocess/__init__.py +57 -0
  3. openprocess/analysis/__init__.py +0 -0
  4. openprocess/analysis/state_space.py +521 -0
  5. openprocess/analysis/state_space_process.py +251 -0
  6. openprocess/cli.py +742 -0
  7. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
  8. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
  9. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
  10. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
  11. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
  12. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
  13. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
  14. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
  15. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
  16. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
  17. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
  18. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
  19. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
  20. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
  21. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
  22. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
  23. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
  24. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
  25. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
  26. openprocess/exercises/pack.md +14 -0
  27. openprocess/flow/__init__.py +50 -0
  28. openprocess/flow/box.py +466 -0
  29. openprocess/flow/boxes/__init__.py +7 -0
  30. openprocess/flow/boxes/check.py +119 -0
  31. openprocess/flow/boxes/compare.py +16 -0
  32. openprocess/flow/boxes/cpn.py +53 -0
  33. openprocess/flow/boxes/discover.py +124 -0
  34. openprocess/flow/boxes/filter.py +80 -0
  35. openprocess/flow/boxes/input.py +124 -0
  36. openprocess/flow/boxes/output.py +52 -0
  37. openprocess/flow/boxes/predict.py +186 -0
  38. openprocess/flow/boxes/science.py +159 -0
  39. openprocess/flow/boxes/sweeps.py +18 -0
  40. openprocess/flow/convert.py +187 -0
  41. openprocess/flow/datasets.py +198 -0
  42. openprocess/flow/explain.py +115 -0
  43. openprocess/flow/library.py +222 -0
  44. openprocess/flow/record.py +385 -0
  45. openprocess/flow/runner.py +357 -0
  46. openprocess/flow/sweep.py +92 -0
  47. openprocess/flow/types.py +290 -0
  48. openprocess/flow/workflow.py +628 -0
  49. openprocess/gui/__init__.py +0 -0
  50. openprocess/gui/app.py +90 -0
  51. openprocess/gui/arc_editing.py +295 -0
  52. openprocess/gui/canvas.py +1414 -0
  53. openprocess/gui/flow/__init__.py +8 -0
  54. openprocess/gui/flow/canvas.py +854 -0
  55. openprocess/gui/flow/page.py +972 -0
  56. openprocess/gui/flow/templates.py +131 -0
  57. openprocess/gui/flow/viewers.py +665 -0
  58. openprocess/gui/items.py +1275 -0
  59. openprocess/gui/learn/answer_boxes.py +978 -0
  60. openprocess/gui/learn/concealment.py +91 -0
  61. openprocess/gui/learn/mode.py +1181 -0
  62. openprocess/gui/panning.py +241 -0
  63. openprocess/gui/resources/openprocess-icon.png +0 -0
  64. openprocess/gui/studio/__init__.py +1 -0
  65. openprocess/gui/studio/__main__.py +3 -0
  66. openprocess/gui/studio/app.py +4031 -0
  67. openprocess/gui/studio/charts.py +115 -0
  68. openprocess/gui/studio/compare_page.py +487 -0
  69. openprocess/gui/studio/cpn_page.py +1858 -0
  70. openprocess/gui/studio/definition_view.py +284 -0
  71. openprocess/gui/studio/derivation_view.py +421 -0
  72. openprocess/gui/studio/documents.py +152 -0
  73. openprocess/gui/studio/dotted_chart.py +1401 -0
  74. openprocess/gui/studio/file_dialogs.py +143 -0
  75. openprocess/gui/studio/filter_dialog.py +247 -0
  76. openprocess/gui/studio/graph_builders.py +176 -0
  77. openprocess/gui/studio/graph_view.py +682 -0
  78. openprocess/gui/studio/instances.py +413 -0
  79. openprocess/gui/studio/log_editor.py +675 -0
  80. openprocess/gui/studio/log_page.py +800 -0
  81. openprocess/gui/studio/markdown_view.py +127 -0
  82. openprocess/gui/studio/mathtext.py +260 -0
  83. openprocess/gui/studio/ml_highlighter.py +75 -0
  84. openprocess/gui/studio/model_page.py +760 -0
  85. openprocess/gui/studio/net_comparison.py +124 -0
  86. openprocess/gui/studio/notes_overlay.py +275 -0
  87. openprocess/gui/studio/petri_page.py +844 -0
  88. openprocess/gui/studio/regions_view.py +502 -0
  89. openprocess/gui/studio/sidebar.py +149 -0
  90. openprocess/gui/studio/style.py +503 -0
  91. openprocess/gui/studio/tool_icons.py +134 -0
  92. openprocess/gui/studio/updates.py +439 -0
  93. openprocess/gui/studio/widgets.py +899 -0
  94. openprocess/gui/studio/workers.py +60 -0
  95. openprocess/gui/studio/workspace.py +447 -0
  96. openprocess/gui/theme.py +394 -0
  97. openprocess/gui/tidy.py +86 -0
  98. openprocess/io/__init__.py +0 -0
  99. openprocess/io/cpn_reader.py +389 -0
  100. openprocess/io/cpn_writer.py +357 -0
  101. openprocess/learn/__init__.py +23 -0
  102. openprocess/learn/answers.py +188 -0
  103. openprocess/learn/checks.py +953 -0
  104. openprocess/learn/computed.py +1180 -0
  105. openprocess/learn/context.py +145 -0
  106. openprocess/learn/exam.py +169 -0
  107. openprocess/learn/exercise-packs.md +325 -0
  108. openprocess/learn/importer.py +216 -0
  109. openprocess/learn/notation.py +474 -0
  110. openprocess/learn/pack.py +511 -0
  111. openprocess/learn/sheet.py +296 -0
  112. openprocess/mining/__init__.py +73 -0
  113. openprocess/mining/analysis.py +689 -0
  114. openprocess/mining/columns.py +282 -0
  115. openprocess/mining/compare_nets.py +246 -0
  116. openprocess/mining/conformance/__init__.py +0 -0
  117. openprocess/mining/conformance/alignments.py +263 -0
  118. openprocess/mining/conformance/quality.py +145 -0
  119. openprocess/mining/conformance/token_replay.py +252 -0
  120. openprocess/mining/csv_import.py +222 -0
  121. openprocess/mining/definitions.py +584 -0
  122. openprocess/mining/dfg.py +187 -0
  123. openprocess/mining/discovery/__init__.py +0 -0
  124. openprocess/mining/discovery/alpha.py +168 -0
  125. openprocess/mining/discovery/heuristics.py +332 -0
  126. openprocess/mining/discovery/inductive.py +477 -0
  127. openprocess/mining/discovery/state_regions.py +62 -0
  128. openprocess/mining/filtering.py +237 -0
  129. openprocess/mining/footprint.py +183 -0
  130. openprocess/mining/invariants.py +191 -0
  131. openprocess/mining/layout.py +279 -0
  132. openprocess/mining/log.py +364 -0
  133. openprocess/mining/petrinet.py +354 -0
  134. openprocess/mining/playout.py +75 -0
  135. openprocess/mining/pm4py_bridge.py +82 -0
  136. openprocess/mining/pnml.py +223 -0
  137. openprocess/mining/processtree.py +216 -0
  138. openprocess/mining/regions.py +476 -0
  139. openprocess/mining/stats.py +160 -0
  140. openprocess/mining/structure.py +374 -0
  141. openprocess/mining/transition_system.py +409 -0
  142. openprocess/mining/xes.py +399 -0
  143. openprocess/ml/__init__.py +0 -0
  144. openprocess/ml/ast_nodes.py +332 -0
  145. openprocess/ml/builtins.py +364 -0
  146. openprocess/ml/colorsets.py +522 -0
  147. openprocess/ml/errors.py +60 -0
  148. openprocess/ml/evaluator.py +754 -0
  149. openprocess/ml/lexer.py +277 -0
  150. openprocess/ml/multiset.py +417 -0
  151. openprocess/ml/parser.py +737 -0
  152. openprocess/ml/values.py +319 -0
  153. openprocess/model/__init__.py +0 -0
  154. openprocess/model/declarations.py +617 -0
  155. openprocess/model/examples.py +98 -0
  156. openprocess/model/net.py +701 -0
  157. openprocess/model/plain.py +192 -0
  158. openprocess/references.py +280 -0
  159. openprocess/sim/__init__.py +0 -0
  160. openprocess/sim/binding.py +620 -0
  161. openprocess/sim/export.py +66 -0
  162. openprocess/sim/simulator.py +315 -0
  163. openprocess/teaching/__init__.py +4 -0
  164. openprocess/teaching/answers.py +4 -0
  165. openprocess/teaching/checks.py +5 -0
  166. openprocess/teaching/pack.py +4 -0
  167. openprocess/teaching/sheet.py +4 -0
  168. openprocess-0.7.0.dist-info/METADATA +927 -0
  169. openprocess-0.7.0.dist-info/RECORD +173 -0
  170. openprocess-0.7.0.dist-info/WHEEL +5 -0
  171. openprocess-0.7.0.dist-info/entry_points.txt +6 -0
  172. openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
  173. openprocess-0.7.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,251 @@
1
+ """Compute a state space in a separate process.
2
+
3
+ Why a process and not a thread
4
+ ------------------------------
5
+ State space exploration is pure Python. In a thread it competes with the
6
+ window for Python's global interpreter lock: every paint and every click
7
+ handler has to wait for the lock, so the whole app stutters for as long as
8
+ the exploration runs. A separate process has its own interpreter, so the
9
+ window stays fully responsive -- and it can be stopped at any moment.
10
+
11
+ The model travels to the worker as a ``.cpn`` file (the same format the app
12
+ saves), and the results come back as plain data (numbers, lists, strings),
13
+ which is everything the GUI shows.
14
+
15
+ The worker is ``python -m openprocess.analysis.state_space_process`` started with
16
+ the same interpreter, rather than :mod:`multiprocessing`, which would
17
+ re-import the GUI's main script in the child.
18
+
19
+ Inside a standalone app (built with PyInstaller, see ``packaging/``) there is
20
+ no separate Python to run: ``sys.executable`` is the app itself. The app is
21
+ then started again with :data:`WORKER_FLAG` as its first argument, and its
22
+ entry script hands those runs to :func:`run_worker` instead of opening a
23
+ window (:func:`worker_command` picks the right form).
24
+
25
+ Usage::
26
+
27
+ job = StateSpaceJob(net, max_nodes=20_000)
28
+ job.start()
29
+ ... # poll job.progress() / job.result() from a timer
30
+ job.cancel() # ask it to stop; it returns what it has found
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import os
36
+ import subprocess
37
+ import sys
38
+ import tempfile
39
+ import threading
40
+ from pathlib import Path
41
+ from typing import Any
42
+
43
+
44
+ def summarise(space, graph_limit: int = 200) -> dict[str, Any]:
45
+ """Everything the GUI displays, as picklable data."""
46
+ net = space.net
47
+ places = list(net.all_places())
48
+ partial = space.partial
49
+ bounds = space.integer_bounds()
50
+ result: dict[str, Any] = {
51
+ "nodes": space.node_count,
52
+ "arcs": space.arc_count,
53
+ "partial": partial,
54
+ "cancelled": space.cancelled,
55
+ "unexplored": space.unexplored_count,
56
+ "sccs": len(space.strongly_connected_components()),
57
+ "dead": space.dead_markings(),
58
+ "home": None if partial else space.home_markings(),
59
+ "dead_transitions": [t.name for t in space.dead_transitions()],
60
+ "live": None if partial else [t.name for t in space.live_transitions()],
61
+ "bounds": [(p.id, p.name, *bounds[p.id]) for p in places],
62
+ "multiset": {k: str(v) for k, v in space.upper_multiset_bounds().items()},
63
+ "report": space.report(),
64
+ "graph": None,
65
+ }
66
+ if space.node_count <= graph_limit:
67
+ dead = set(result["dead"])
68
+ nodes = [(index, space.describe_state(index), index in dead)
69
+ for index in range(space.node_count)]
70
+ arcs = [(arc.source, arc.target, arc.binding.describe(net))
71
+ for arcs in space.arcs.values() for arc in arcs]
72
+ result["graph"] = (nodes, arcs)
73
+ return result
74
+
75
+
76
+ #: First argument that makes a standalone app run as a state space worker.
77
+ WORKER_FLAG = "--openprocess-state-space-worker"
78
+
79
+
80
+ def worker_command(*arguments: str) -> list[str]:
81
+ """The command line that starts a worker with ``arguments``.
82
+
83
+ From source (or a pip/conda install) that is this module run by the same
84
+ Python. In a frozen app ``sys.executable`` is the app's own program, so
85
+ the app is re-launched with :data:`WORKER_FLAG` instead.
86
+ """
87
+ if getattr(sys, "frozen", False):
88
+ return [sys.executable, WORKER_FLAG, *arguments]
89
+ return [sys.executable, "-m", "openprocess.analysis.state_space_process", *arguments]
90
+
91
+
92
+ def run_worker(arguments: list[str]) -> int:
93
+ """Run the worker on ``[MODEL, MAX_NODES, OUT]`` (the arguments after the flag)."""
94
+ model_path, max_nodes, output_path = arguments
95
+ return _child(model_path, int(max_nodes), output_path)
96
+
97
+
98
+ def _child(model_path: str, max_nodes: int, output_path: str) -> int:
99
+ """The worker: ``python -m openprocess.analysis.state_space_process MODEL N OUT``.
100
+
101
+ Talks to the parent through its standard streams: it prints
102
+ ``progress <nodes> <arcs>`` lines and ``phase <name>`` while working, and
103
+ stops early when the parent writes ``stop``. The result is pickled to
104
+ ``OUT``, then ``done`` (or ``error <message>``) is printed.
105
+ """
106
+ import pickle
107
+ import sys
108
+ import threading
109
+ import time
110
+
111
+ stop = threading.Event()
112
+ # The parent reads UTF-8; Windows pipes would otherwise use the ANSI code page.
113
+ for stream in (sys.stdout, sys.stdin):
114
+ if hasattr(stream, "reconfigure"):
115
+ stream.reconfigure(encoding="utf-8")
116
+
117
+ def listen() -> None:
118
+ for line in sys.stdin:
119
+ if line.strip() == "stop":
120
+ stop.set()
121
+ stop.set() # the parent went away: stop too
122
+
123
+ threading.Thread(target=listen, daemon=True).start()
124
+
125
+ def say(text: str) -> None:
126
+ print(text, flush=True)
127
+
128
+ try:
129
+ from ..io.cpn_reader import read_cpn
130
+ from .state_space import StateSpace
131
+ net = read_cpn(model_path)
132
+ problems = net.compile()
133
+ if problems:
134
+ say("error " + "; ".join(str(p) for p in problems[:3]).replace("\n", " "))
135
+ return 1
136
+ last = [0.0]
137
+
138
+ def progress(node_count: int, arc_count: int) -> None:
139
+ now = time.monotonic()
140
+ if now - last[0] > 0.15:
141
+ last[0] = now
142
+ say(f"progress {node_count} {arc_count}")
143
+
144
+ space = StateSpace(net).generate(max_nodes=max_nodes, should_stop=stop.is_set,
145
+ progress=progress)
146
+ say(f"progress {space.node_count} {space.arc_count}")
147
+ say("phase Analysing")
148
+ with open(output_path, "wb") as handle:
149
+ pickle.dump(summarise(space), handle)
150
+ say("done")
151
+ return 0
152
+ except Exception as error: # noqa: BLE001 - report anything to the GUI
153
+ say(f"error {type(error).__name__}: {error}".replace("\n", " "))
154
+ return 1
155
+
156
+
157
+ class StateSpaceJob:
158
+ """One state space computation in a child process."""
159
+
160
+ def __init__(self, net, max_nodes: int = 20_000) -> None:
161
+ from ..io.cpn_writer import write_cpn
162
+ directory = tempfile.mkdtemp(prefix="openprocess-space-")
163
+ self.model_path = os.path.join(directory, "model.cpn")
164
+ self.output_path = os.path.join(directory, "result.pickle")
165
+ self.directory = directory
166
+ write_cpn(net, Path(self.model_path))
167
+ self.max_nodes = max_nodes
168
+ self.process: subprocess.Popen | None = None
169
+ self.nodes = self.arcs = 0
170
+ self.phase = "Starting"
171
+ self.outcome: tuple[str, Any] | None = None
172
+ self._message: str | None = None
173
+
174
+ def start(self) -> None:
175
+ # A fresh interpreter running this module: nothing of the GUI's state
176
+ # (Qt, open windows) is inherited.
177
+ # From source, run from the project folder so ``-m openprocess...`` resolves
178
+ # even without an install; a frozen app finds its code by itself.
179
+ cwd = None if getattr(sys, "frozen", False) else str(Path(__file__).resolve().parents[2])
180
+ self.process = subprocess.Popen(
181
+ worker_command(self.model_path, str(self.max_nodes), self.output_path),
182
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
183
+ text=True, encoding="utf-8", bufsize=1, cwd=cwd,
184
+ # Windows: don't flash a console window for the worker.
185
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
186
+ self.phase = "Exploring"
187
+ threading.Thread(target=self._read, daemon=True).start()
188
+
189
+ def _read(self) -> None:
190
+ """Reader thread: parse the child's messages (blocks on I/O only)."""
191
+ assert self.process is not None and self.process.stdout is not None
192
+ for line in self.process.stdout:
193
+ word, _, rest = line.strip().partition(" ")
194
+ if word == "progress":
195
+ parts = rest.split()
196
+ if len(parts) == 2:
197
+ self.nodes, self.arcs = int(parts[0]), int(parts[1])
198
+ elif word == "phase":
199
+ self.phase = rest
200
+ elif word in ("done", "error"):
201
+ self._message = word + (" " + rest if rest else "")
202
+
203
+ def progress(self) -> tuple[int, int, str]:
204
+ return self.nodes, self.arcs, self.phase
205
+
206
+ def poll(self) -> tuple[str, Any] | None:
207
+ """``("done", summary)``, ``("error", message)``, or ``None`` while running."""
208
+ if self.outcome is not None or self.process is None:
209
+ return self.outcome
210
+ if self.process.poll() is None:
211
+ return None
212
+ # The child has exited; make sure its last lines have been read.
213
+ for _ in range(50):
214
+ if self._message is not None:
215
+ break
216
+ import time
217
+ time.sleep(0.01)
218
+ message = self._message or ""
219
+ if message == "done":
220
+ import pickle
221
+ with open(self.output_path, "rb") as handle:
222
+ self.outcome = ("done", pickle.load(handle))
223
+ elif message.startswith("error"):
224
+ self.outcome = ("error", message[6:] or "unknown error")
225
+ else:
226
+ self.outcome = ("error", "stopped")
227
+ self._cleanup()
228
+ return self.outcome
229
+
230
+ def cancel(self) -> None:
231
+ """Ask the worker to stop; it then reports what it has explored."""
232
+ if self.process is not None and self.process.poll() is None:
233
+ try:
234
+ self.process.stdin.write("stop\n")
235
+ self.process.stdin.flush()
236
+ except (OSError, ValueError):
237
+ pass
238
+
239
+ def kill(self) -> None:
240
+ """Stop at once, without results (closing the page, a stuck worker)."""
241
+ if self.process is not None and self.process.poll() is None:
242
+ self.process.kill()
243
+ self._cleanup()
244
+
245
+ def _cleanup(self) -> None:
246
+ import shutil
247
+ shutil.rmtree(self.directory, ignore_errors=True)
248
+
249
+
250
+ if __name__ == "__main__": # the child process
251
+ raise SystemExit(run_worker(sys.argv[1:4]))