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,216 @@
1
+ """Turning a past exam into a pack: the guided import.
2
+
3
+ Paste the exam's text (or give the file); :func:`split_questions` finds the
4
+ questions and their parts by their numbering, :func:`guess_type` picks an
5
+ answer block for each part from how it is worded, and :func:`write_pack`
6
+ writes the skeleton: one exercise folder per question with a
7
+ ``question.md`` whose blocks are ready for the author to finish. Each
8
+ block carries a ``TODO`` where the author must act: drop in the net or the
9
+ log as a file, replace a guessed ``compute:`` with the right one, or write a
10
+ ``solution:`` for an open question. ``openprocess exercises check`` then lists
11
+ what is still missing.
12
+
13
+ The import never computes answers itself: it cannot know the net or the log
14
+ the exam showed as a picture. Its job is the structure, so that the author
15
+ types nothing twice.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+
24
+ #: The start of a question: "1.", "Question 2", "Q3", "2)" at the start of a line.
25
+ _QUESTION = re.compile(r"^\s*(?:question|q|exercise|opgave)?\s*(\d{1,2})\s*[.):]\s*(.*)$", re.I)
26
+ #: The start of a part: "a.", "(a)", "a)", "1a".
27
+ _PART = re.compile(r"^\s*\(?(?:\d{1,2})?([a-z])[.)]\s+(.*)$", re.I)
28
+ #: "(2 points)", "[3 pt]", "(1.5 punten)"
29
+ _POINTS = re.compile(r"[\(\[]\s*(\d+(?:[.,]\d+)?)\s*(?:points?|pts?|punten|p)\s*[\)\]]", re.I)
30
+
31
+
32
+ @dataclass
33
+ class Part:
34
+ letter: str
35
+ text: str
36
+ points: float | None = None
37
+
38
+ @property
39
+ def kind(self) -> str:
40
+ return guess_type(self.text)
41
+
42
+
43
+ @dataclass
44
+ class Question:
45
+ number: int
46
+ title: str
47
+ intro: str = ""
48
+ parts: list[Part] = field(default_factory=list)
49
+ points: float | None = None
50
+
51
+
52
+ def _points(text: str) -> tuple[float | None, str]:
53
+ match = _POINTS.search(text)
54
+ if not match:
55
+ return None, text
56
+ return float(match.group(1).replace(",", ".")), (text[:match.start()] + text[match.end():]).strip()
57
+
58
+
59
+ def split_questions(text: str) -> list[Question]:
60
+ """The questions of an exam, each with its lettered parts."""
61
+ questions: list[Question] = []
62
+ current: Question | None = None
63
+ part: Part | None = None
64
+ for raw in text.splitlines():
65
+ line = raw.rstrip()
66
+ question = _QUESTION.match(line)
67
+ if question and (current is None or int(question.group(1)) == current.number + 1):
68
+ points, title = _points(question.group(2).strip())
69
+ current = Question(int(question.group(1)), title or f"Question {question.group(1)}",
70
+ points=points)
71
+ questions.append(current)
72
+ part = None
73
+ continue
74
+ if current is None:
75
+ continue
76
+ found = _PART.match(line)
77
+ expected = chr(ord(current.parts[-1].letter) + 1) if current.parts else "a"
78
+ if found and found.group(1).lower() == expected:
79
+ points, body = _points(found.group(2).strip())
80
+ part = Part(expected, body, points)
81
+ current.parts.append(part)
82
+ continue
83
+ if part is not None:
84
+ part.text = (part.text + "\n" + line).strip()
85
+ else:
86
+ current.intro = (current.intro + "\n" + line).strip()
87
+ return questions
88
+
89
+
90
+ #: Wording → block type, first match wins.
91
+ _CUES: list[tuple[re.Pattern, str]] = [
92
+ (re.compile(r"\b(draw|model|construct|design|complete|repair|change)\b.*\b(net|petri)", re.I | re.S), "net"),
93
+ (re.compile(r"\b(draw|give|construct)\b.*\b(reachability|marking) graph|transition system\b", re.I | re.S), "ts"),
94
+ (re.compile(r"\bfootprint\b|\brelation(s)? between\b|\b(causal|parallel|choice) relation", re.I), "footprint"),
95
+ (re.compile(r"\b(incidence|matrix|matrices)\b", re.I), "matrix"),
96
+ (re.compile(r"\balignment\b.*\b(optimal|sub-?optimal|not an alignment)", re.I | re.S), "choice"),
97
+ (re.compile(r"\b(give|write|construct)\b.*\balignment\b", re.I | re.S), "alignment"),
98
+ (re.compile(r"\bprocess tree\b", re.I), "tree"),
99
+ (re.compile(r"\b(cut)\b.*\binductive\b|\binductive\b.*\bcut\b", re.I | re.S), "cut"),
100
+ (re.compile(r"\b(sublog|split the log|projected log)\b", re.I), "log"),
101
+ (re.compile(r"\b(produced|consumed|missing|remaining)\b", re.I), "replay"),
102
+ (re.compile(r"\b(rank|order)\b.*\b(models?|nets?)\b", re.I | re.S), "ranking"),
103
+ (re.compile(r"\b(firing sequence|trace)\b.*\b(deadlock|dead marking|not in the log|never)", re.I | re.S), "trace"),
104
+ (re.compile(r"\b(reachable|terminal|dead) markings\b", re.I), "markings"),
105
+ (re.compile(r"\bmarking\b.*\b(after|reached|firing)", re.I | re.S), "marking"),
106
+ (re.compile(r"\(P\s*,\s*T\s*,\s*F|\bformalise\b|\bformalize\b|\bpreset\b|\bpostset\b", re.I), "tuple"),
107
+ (re.compile(r"\bwhich of the following\b|\bchoose\b|\bselect\b|\bmultiple[- ]choice\b", re.I), "choice"),
108
+ (re.compile(r"\bhow many\b|\bcompute\b|\bcalculate\b|\bfitness\b|\bnumber of\b|\bprobability\b", re.I), "number"),
109
+ (re.compile(r"\b(is|are|does|do|can|could|has|have|will)\b\s+(the|this|it|there|every|each|any)\b.*\?", re.I | re.S), "yesno"),
110
+ (re.compile(r"\b(true or false|yes or no)\b", re.I), "yesno"),
111
+ (re.compile(r"\b(give|list|which|what are|determine)\b.*\b(set|activities|transitions|places|states|regions|pairs)\b", re.I | re.S), "set"),
112
+ (re.compile(r"\b(explain|why|argue|motivate|describe|discuss)\b", re.I), "open"),
113
+ ]
114
+
115
+
116
+ def guess_type(text: str) -> str:
117
+ """The answer block a question's wording suggests (``open`` when unsure)."""
118
+ for pattern, kind in _CUES:
119
+ if pattern.search(text):
120
+ return kind
121
+ return "open"
122
+
123
+
124
+ _COMPUTE_HINT = {
125
+ "yesno": "compute: sound # TODO: the property asked (sound, live, bounded, dead(t)…)",
126
+ "number": "compute: fitness # TODO: the figure asked (fitness, deadlocks, bound, cases…)",
127
+ "set": "compute: alpha.T_I # TODO: the set asked (dead transitions, enabled([p1]), net.T…)",
128
+ "footprint": "pairs: (a, b), (b, c) # TODO: only the pairs the exam asks (or remove for all)",
129
+ "matrix": "compute: incidence # TODO: incidence, language.M or language.M'",
130
+ "ts": "compute: reachability graph",
131
+ "markings": "compute: reachable # TODO: reachable or terminal",
132
+ "marking": "compute: fire(a, [p1]) # TODO: the sequence and the start marking",
133
+ "tuple": "",
134
+ "tree": "compute: im.tree",
135
+ "cut": "compute: im.cut # TODO: im.cut(2) for a deeper step",
136
+ "log": "compute: im.split(1) # TODO: which part of which cut",
137
+ "replay": "",
138
+ "alignment": "trace: a, b, c # TODO: the trace to align",
139
+ "ranking": "over: m1.pnml, m2.pnml # TODO: the nets to rank, as files\nby: fitness",
140
+ "trace": "ends: deadlock # TODO: any, final, deadlock, stuck or improper",
141
+ "net": "answer: answer.pnml # TODO: draw the model answer and save it as answer.pnml\nsound: yes",
142
+ "choice": "- [ ] TODO: the first option\n- [ ] TODO: the second option",
143
+ "open": "solution: TODO: write the model answer here",
144
+ }
145
+
146
+
147
+ def block_for(part: Part) -> str:
148
+ """The answer block (as text) for a part of a question."""
149
+ kind = part.kind
150
+ lines = [f"type: {kind}"]
151
+ hint = _COMPUTE_HINT.get(kind, "")
152
+ if hint:
153
+ lines.extend(hint.split("\n"))
154
+ if part.points is not None:
155
+ lines.append(f"points: {part.points:g}")
156
+ if kind != "open" and "solution" not in hint:
157
+ lines.append("solution: TODO: the grading scheme's answer, in words")
158
+ return "```answer\n" + "\n".join(lines) + "\n```"
159
+
160
+
161
+ def question_markdown(question: Question) -> str:
162
+ """``question.md`` for one question of the exam."""
163
+ points = question.points if question.points is not None else \
164
+ (sum(p.points for p in question.parts if p.points) or None)
165
+ title = f"# {question.number} · {question.title}" + (f" ({points:g} points)" if points else "")
166
+ out = []
167
+ # The question's points, when its parts do not add up to them (none given,
168
+ # or a different total): front matter keeps the exam's total right.
169
+ if points and sum(p.points or 0 for p in question.parts) != points:
170
+ out += ["---", f"points: {points:g}", "---"]
171
+ out += [title, ""]
172
+ if question.intro:
173
+ out += [question.intro, "", "> **TODO:** the exam showed a net or a log here: recreate it as "
174
+ "`net.pnml` / `log.txt` in this folder so the app can compute the answers.", ""]
175
+ for part in question.parts:
176
+ out += [f"**{part.letter}.** {part.text}", "", block_for(part), ""]
177
+ if not question.parts:
178
+ out += [block_for(Part("a", question.intro or question.title)), ""]
179
+ return "\n".join(out).rstrip() + "\n"
180
+
181
+
182
+ def write_pack(text: str, target: str | Path, title: str = "Exam") -> list[Path]:
183
+ """Write the skeleton pack for an exam's text; the files written."""
184
+ target = Path(target)
185
+ target.mkdir(parents=True, exist_ok=True)
186
+ questions = split_questions(text)
187
+ if not questions:
188
+ raise ValueError("no questions found: number them 1., 2., … with parts a., b., …")
189
+ written = []
190
+ pack_file = target / "pack.md"
191
+ if not pack_file.exists():
192
+ total = sum(q.points or sum(p.points for p in q.parts if p.points) or 0 for q in questions)
193
+ pack_file.write_text("---\nexam: yes\ntime: 180\n---\n" + f"# {title}\n\n"
194
+ f"{len(questions)} questions" + (f", {total:g} points" if total else "")
195
+ + ". Made with *Make a pack from an exam*: every `TODO` in the "
196
+ "questions is for the author to finish, then run "
197
+ "`openprocess exercises check` on this folder.\n", encoding="utf-8")
198
+ written.append(pack_file)
199
+ for question in questions:
200
+ name = re.sub(r"[^\w\s\-·]+", "", question.title).strip() or f"Question {question.number}"
201
+ folder = target / f"{question.number} {name[:60]}"
202
+ folder.mkdir(exist_ok=True)
203
+ path = folder / "question.md"
204
+ path.write_text(question_markdown(question), encoding="utf-8")
205
+ written.append(path)
206
+ return written
207
+
208
+
209
+ def todo_list(target: str | Path) -> list[str]:
210
+ """Every ``TODO`` left in a pack, with its file and line."""
211
+ found = []
212
+ for path in sorted(Path(target).rglob("question.md")):
213
+ for number, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
214
+ if "TODO" in line:
215
+ found.append(f"{path.parent.name}/question.md:{number}: {line.strip()}")
216
+ return found