openprocess 0.16.1__tar.gz → 0.16.2__tar.gz

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 (271) hide show
  1. {openprocess-0.16.1/openprocess.egg-info → openprocess-0.16.2}/PKG-INFO +8 -2
  2. {openprocess-0.16.1 → openprocess-0.16.2}/README.md +7 -1
  3. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/__init__.py +1 -1
  4. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/analysis/state_space.py +7 -0
  5. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/check.py +6 -0
  6. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/derivation.py +2 -1
  7. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/figures.py +1 -1
  8. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/report.py +8 -2
  9. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/viewers.py +4 -0
  10. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/learn/answer_boxes.py +25 -5
  11. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/learn/mode.py +8 -3
  12. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/cpn_page.py +10 -5
  13. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/instances.py +2 -0
  14. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/model_page.py +3 -1
  15. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/api.py +2 -0
  16. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/analysis.py +19 -5
  17. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/conformance/alignments.py +28 -6
  18. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/conformance/token_replay.py +36 -2
  19. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/net.py +19 -0
  20. {openprocess-0.16.1 → openprocess-0.16.2/openprocess.egg-info}/PKG-INFO +8 -2
  21. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_exercises_gui.py +56 -0
  22. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_review_fixes.py +86 -0
  23. {openprocess-0.16.1 → openprocess-0.16.2}/LICENSE +0 -0
  24. {openprocess-0.16.1 → openprocess-0.16.2}/cpnpy/__init__.py +0 -0
  25. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/analysis/__init__.py +0 -0
  26. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/analysis/state_space_process.py +0 -0
  27. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/citation.py +0 -0
  28. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/cli.py +0 -0
  29. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
  30. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
  31. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
  32. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
  33. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
  34. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
  35. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
  36. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
  37. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
  38. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
  39. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
  40. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
  41. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
  42. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
  43. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
  44. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
  45. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
  46. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
  47. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
  48. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/pack.md +0 -0
  49. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/__init__.py +0 -0
  50. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/box.py +0 -0
  51. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/__init__.py +0 -0
  52. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/compare.py +0 -0
  53. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/cpn.py +0 -0
  54. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/discover.py +0 -0
  55. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/filter.py +0 -0
  56. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/input.py +0 -0
  57. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/measure.py +0 -0
  58. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/output.py +0 -0
  59. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/predict.py +0 -0
  60. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/science.py +0 -0
  61. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/sweeps.py +0 -0
  62. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/changes.py +0 -0
  63. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/convert.py +0 -0
  64. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/datasets.py +0 -0
  65. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/explain.py +0 -0
  66. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/history.py +0 -0
  67. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/library.py +0 -0
  68. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/loading.py +0 -0
  69. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/parallel.py +0 -0
  70. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/provenance.py +0 -0
  71. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/record.py +0 -0
  72. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/report.py +0 -0
  73. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/reproducibility.py +0 -0
  74. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/runner.py +0 -0
  75. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/script.py +0 -0
  76. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/snapshots.py +0 -0
  77. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/studio.py +0 -0
  78. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/sweep.py +0 -0
  79. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/types.py +0 -0
  80. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/versions.py +0 -0
  81. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/workflow.py +0 -0
  82. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/__init__.py +0 -0
  83. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/app.py +0 -0
  84. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/arc_editing.py +0 -0
  85. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/canvas.py +0 -0
  86. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/__init__.py +0 -0
  87. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/canvas.py +0 -0
  88. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/page.py +0 -0
  89. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/picker.py +0 -0
  90. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/stepper.py +0 -0
  91. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/templates.py +0 -0
  92. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/versions.py +0 -0
  93. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/items.py +0 -0
  94. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/learn/concealment.py +0 -0
  95. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/panning.py +0 -0
  96. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/resources/openprocess-icon.png +0 -0
  97. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/__init__.py +0 -0
  98. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/__main__.py +0 -0
  99. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/alignment_view.py +0 -0
  100. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/app.py +0 -0
  101. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/charts.py +0 -0
  102. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/cite.py +0 -0
  103. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/command_palette.py +0 -0
  104. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/compare_page.py +0 -0
  105. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/connect.py +0 -0
  106. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/connections.py +0 -0
  107. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/definition_view.py +0 -0
  108. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/derivation_view.py +0 -0
  109. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/documents.py +0 -0
  110. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/dotted_chart.py +0 -0
  111. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/file_dialogs.py +0 -0
  112. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/filter_dialog.py +0 -0
  113. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/graph_builders.py +0 -0
  114. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/graph_view.py +0 -0
  115. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/loading.py +0 -0
  116. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/log_editor.py +0 -0
  117. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/log_page.py +0 -0
  118. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/markdown_view.py +0 -0
  119. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/mathtext.py +0 -0
  120. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/ml_highlighter.py +0 -0
  121. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/motion.py +0 -0
  122. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/net_comparison.py +0 -0
  123. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/notes_overlay.py +0 -0
  124. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/petri_page.py +0 -0
  125. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/provenance.py +0 -0
  126. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/regions_view.py +0 -0
  127. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/remote.py +0 -0
  128. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/review.py +0 -0
  129. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/sidebar.py +0 -0
  130. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/spaces.py +0 -0
  131. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/style.py +0 -0
  132. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/tool_icons.py +0 -0
  133. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/updates.py +0 -0
  134. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/widgets.py +0 -0
  135. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/workers.py +0 -0
  136. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/workspace.py +0 -0
  137. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/theme.py +0 -0
  138. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/tidy.py +0 -0
  139. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/io/__init__.py +0 -0
  140. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/io/cpn_reader.py +0 -0
  141. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/io/cpn_writer.py +0 -0
  142. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/__init__.py +0 -0
  143. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/answers.py +0 -0
  144. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/checks.py +0 -0
  145. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/computed.py +0 -0
  146. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/context.py +0 -0
  147. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/exam.py +0 -0
  148. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/exercise-packs.md +0 -0
  149. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/importer.py +0 -0
  150. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/notation.py +0 -0
  151. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/pack.py +0 -0
  152. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/sheet.py +0 -0
  153. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/__init__.py +0 -0
  154. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/__main__.py +0 -0
  155. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/connecting.md +0 -0
  156. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/install.py +0 -0
  157. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/server.py +0 -0
  158. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/__init__.py +0 -0
  159. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/bpmn.py +0 -0
  160. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/cases.py +0 -0
  161. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/columns.py +0 -0
  162. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/compare_nets.py +0 -0
  163. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/conformance/__init__.py +0 -0
  164. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/conformance/quality.py +0 -0
  165. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/csv_import.py +0 -0
  166. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/definitions.py +0 -0
  167. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/dfg.py +0 -0
  168. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/__init__.py +0 -0
  169. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/alpha.py +0 -0
  170. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/heuristics.py +0 -0
  171. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/inductive.py +0 -0
  172. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/state_regions.py +0 -0
  173. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/dotted.py +0 -0
  174. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/drawing.py +0 -0
  175. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/filtering.py +0 -0
  176. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/footprint.py +0 -0
  177. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/invariants.py +0 -0
  178. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/layout.py +0 -0
  179. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/log.py +0 -0
  180. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/ocel.py +0 -0
  181. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/parallel.py +0 -0
  182. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/petrinet.py +0 -0
  183. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/playout.py +0 -0
  184. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/pm4py_bridge.py +0 -0
  185. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/pnml.py +0 -0
  186. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/processtree.py +0 -0
  187. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/regions.py +0 -0
  188. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/stats.py +0 -0
  189. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/structure.py +0 -0
  190. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/transition_system.py +0 -0
  191. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/xes.py +0 -0
  192. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/__init__.py +0 -0
  193. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/ast_nodes.py +0 -0
  194. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/builtins.py +0 -0
  195. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/colorsets.py +0 -0
  196. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/errors.py +0 -0
  197. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/evaluator.py +0 -0
  198. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/lexer.py +0 -0
  199. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/multiset.py +0 -0
  200. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/parser.py +0 -0
  201. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/values.py +0 -0
  202. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/__init__.py +0 -0
  203. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/declarations.py +0 -0
  204. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/examples.py +0 -0
  205. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/plain.py +0 -0
  206. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/packages.py +0 -0
  207. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/palette.py +0 -0
  208. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/parallel.py +0 -0
  209. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/references.py +0 -0
  210. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/__init__.py +0 -0
  211. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/binding.py +0 -0
  212. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/export.py +0 -0
  213. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/performance.py +0 -0
  214. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/simulator.py +0 -0
  215. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/__init__.py +0 -0
  216. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/answers.py +0 -0
  217. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/checks.py +0 -0
  218. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/pack.py +0 -0
  219. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/sheet.py +0 -0
  220. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/SOURCES.txt +0 -0
  221. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/dependency_links.txt +0 -0
  222. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/entry_points.txt +0 -0
  223. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/requires.txt +0 -0
  224. {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/top_level.txt +0 -0
  225. {openprocess-0.16.1 → openprocess-0.16.2}/pyproject.toml +0 -0
  226. {openprocess-0.16.1 → openprocess-0.16.2}/setup.cfg +0 -0
  227. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_arc_editing.py +0 -0
  228. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_bpmn.py +0 -0
  229. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_citation.py +0 -0
  230. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_colorsets.py +0 -0
  231. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_columns.py +0 -0
  232. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_cpn_course_model.py +0 -0
  233. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_definitions.py +0 -0
  234. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_docs.py +0 -0
  235. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_dotted_chart.py +0 -0
  236. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_flow.py +0 -0
  237. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_flow_gui.py +0 -0
  238. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_framework_contract.py +0 -0
  239. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_hierarchy.py +0 -0
  240. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_history.py +0 -0
  241. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_io.py +0 -0
  242. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_large_log_page.py +0 -0
  243. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_large_logs.py +0 -0
  244. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_learn.py +0 -0
  245. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_learn_gui.py +0 -0
  246. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_log_editing.py +0 -0
  247. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_mcp.py +0 -0
  248. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_mcp_gui.py +0 -0
  249. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_measure.py +0 -0
  250. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_mining.py +0 -0
  251. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_ml.py +0 -0
  252. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_multiset.py +0 -0
  253. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_ocel.py +0 -0
  254. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_packages.py +0 -0
  255. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_parallel_and_loading.py +0 -0
  256. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_parallel_boxes.py +0 -0
  257. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_performance.py +0 -0
  258. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_regions.py +0 -0
  259. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_rename.py +0 -0
  260. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_report.py +0 -0
  261. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_researcher_api.py +0 -0
  262. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_simulation.py +0 -0
  263. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_snapshots.py +0 -0
  264. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_state_space.py +0 -0
  265. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_studio_gui.py +0 -0
  266. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_studio_lazy.py +0 -0
  267. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_teaching.py +0 -0
  268. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_tidy.py +0 -0
  269. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_updates.py +0 -0
  270. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_wf_structure.py +0 -0
  271. {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.16.1
3
+ Version: 0.16.2
4
4
  Summary: Process mining, Petri nets and workflows in one open app: every algorithm visible, every result reproducible, every step teachable
5
5
  Author: Sam Struthers
6
6
  License: MIT
@@ -742,6 +742,9 @@ course).
742
742
 
743
743
  ## For developers
744
744
 
745
+ Want to report a wrong result, add an algorithm or fix something? See
746
+ [**CONTRIBUTING.md**](CONTRIBUTING.md).
747
+
745
748
  ```bash
746
749
  pytest -q # the whole suite, including GUI tests that run offscreen
747
750
  openprocess --help # command line: check, simulate, state space, mining
@@ -1089,7 +1092,10 @@ Plainly stated:
1089
1092
  each use its own copy of the page. Subpages used once, at any depth,
1090
1093
  simulate.
1091
1094
  2. **No CPN monitors or simulation reports**, and **no fairness
1092
- properties** in the state space report (it says so).
1095
+ properties** in the state space report (it says so). **Transition
1096
+ priorities and code segments are kept but not run**: a model that uses
1097
+ them simulates differently from CPN Tools, and the Problems tab and the
1098
+ state space report list where.
1093
1099
  3. **CPN ML is a large subset, not all of Standard ML**: no `exception`
1094
1100
  declarations or `handle`, no `datatype` or `structure` declarations, and
1095
1101
  characters are one-letter strings. See
@@ -700,6 +700,9 @@ course).
700
700
 
701
701
  ## For developers
702
702
 
703
+ Want to report a wrong result, add an algorithm or fix something? See
704
+ [**CONTRIBUTING.md**](CONTRIBUTING.md).
705
+
703
706
  ```bash
704
707
  pytest -q # the whole suite, including GUI tests that run offscreen
705
708
  openprocess --help # command line: check, simulate, state space, mining
@@ -1047,7 +1050,10 @@ Plainly stated:
1047
1050
  each use its own copy of the page. Subpages used once, at any depth,
1048
1051
  simulate.
1049
1052
  2. **No CPN monitors or simulation reports**, and **no fairness
1050
- properties** in the state space report (it says so).
1053
+ properties** in the state space report (it says so). **Transition
1054
+ priorities and code segments are kept but not run**: a model that uses
1055
+ them simulates differently from CPN Tools, and the Problems tab and the
1056
+ state space report list where.
1051
1057
  3. **CPN ML is a large subset, not all of Standard ML**: no `exception`
1052
1058
  declarations or `handle`, no `datatype` or `structure` declarations, and
1053
1059
  characters are one-letter strings. See
@@ -48,7 +48,7 @@ from .sim.simulator import Simulator
48
48
  #: The one place the version number is written (pyproject.toml, the update
49
49
  #: check and packaging/build.py read it from here). A release is tagged
50
50
  #: "v" + this, e.g. v0.2.0.
51
- __version__ = "0.16.1"
51
+ __version__ = "0.16.2"
52
52
 
53
53
  __all__ = [
54
54
  "Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
@@ -451,6 +451,13 @@ class StateSpace:
451
451
  if self.partial:
452
452
  add(" WARNING: the properties below describe only the explored")
453
453
  add(" fragment and are not proofs about the whole model.")
454
+ ignored = getattr(self.net, "warnings", [])
455
+ if ignored:
456
+ add(f" WARNING: {len(ignored)} transition priorities or code segments are not")
457
+ add(" implemented and were not used: this state space can differ")
458
+ add(" from the one CPN Tools computes.")
459
+ for issue in ignored[:max_listed]:
460
+ add(f" {issue.element_name or issue.element_id} ({issue.field_name})")
454
461
  add("")
455
462
 
456
463
  add("Boundedness Properties")
@@ -48,6 +48,12 @@ def check_fit(model: PetriNet, log: EventLog,
48
48
  scores = Scores(model.name, {"fitness": fitness, "precision": precision(model, simple),
49
49
  "generalisation": generalisation(replay), "simplicity": simplicity(model)})
50
50
  scores.note = f"Checked against all {replay.trace_count} cases of {log.name} ({method.lower()})."
51
+ if replay.unknown_events:
52
+ missing = ", ".join(f"{a} ({n:,})" for a, n in replay.unknown_totals().most_common(5))
53
+ text = (f"{replay.unknown_events:,} events are of activities the model does not have: {missing}"
54
+ f"{', …' if len(replay.unknown_totals()) > 5 else ''}.")
55
+ flow.note(text + " In token replay each counts one missing and one remaining token.")
56
+ scores.note += " " + text
51
57
  if method == "Alignments" and alignments.incomplete:
52
58
  cases = sum(a.count for a in alignments.incomplete)
53
59
  scores.note += (f" No optimal alignment was found for {len(alignments.incomplete)} variants "
@@ -94,7 +94,8 @@ def value_markdown(value: Any, limit: int = 50) -> str:
94
94
  + _md_table(["Variant", "Cases", "p", "c", "m", "r"], rows))
95
95
  if isinstance(value, AlignmentResult):
96
96
  rows = [[f"⟨{', '.join(a.trace)}⟩", a.count, a.cost, f"{a.fitness:.3f}"] for a in value.alignments[:limit]]
97
- return (f"Fitness {value.average_fitness:.4f}; {value.fitting_traces:,} of {value.trace_count:,} cases "
97
+ return (f"Fitness {'' if value.exact else 'at least '}{value.average_fitness:.4f}; "
98
+ f"{value.fitting_traces:,} of {value.trace_count:,} cases "
98
99
  f"align without a deviation\n\n" + _md_table(["Variant", "Cases", "Cost", "Fitness"], rows))
99
100
  if isinstance(value, OCEL):
100
101
  return value.summary()
@@ -49,7 +49,7 @@ def tile_parts(value) -> tuple[str, str] | None:
49
49
  if isinstance(value, ReplayResult):
50
50
  return f"{value.fitness:.2f} fitness", f"{value.fitting_traces:,} of {value.trace_count:,} cases fit"
51
51
  if isinstance(value, AlignmentResult):
52
- return (f"{value.average_fitness:.2f} fitness",
52
+ return (f"{'' if value.exact else '≥ '}{value.average_fitness:.2f} fitness",
53
53
  f"{value.fitting_traces:,} of {value.trace_count:,} cases align")
54
54
  if isinstance(value, Text):
55
55
  first = str(value).strip().splitlines()[0] if str(value).strip() else ""
@@ -293,8 +293,14 @@ class Section(QFrame):
293
293
  if result is not None and result.status == DONE:
294
294
  parts = key_figure(result.value)
295
295
  if parts is not None:
296
- tile = StatTile("Key figure", *parts)
297
- tile.setMaximumWidth(320)
296
+ figure, line = parts
297
+ if len(line) > 72:
298
+ line = line[:71].rstrip() + "…"
299
+ tile = StatTile("Key figure", figure, line)
300
+ # One line each, the tile as wide as they need: a wrapped line in a
301
+ # tile of fixed height was drawn over the figure above it.
302
+ tile.value.setWordWrap(False)
303
+ tile.sub.setWordWrap(False)
298
304
  self.body_layout.addWidget(tile, 0, Qt.AlignLeft)
299
305
  self.body_widget = page.result_view(node, result)
300
306
  self.body_layout.addWidget(self.body_widget)
@@ -362,6 +362,10 @@ def result_widget(value, page=None, node=None) -> QWidget:
362
362
  add(label(f"Fitness {value.fitness:.4f}: p = {value.produced}, c = {value.consumed}, "
363
363
  f"m = {value.missing}, r = {value.remaining}; {value.fitting_traces} of "
364
364
  f"{value.trace_count} cases fit.", "muted", wrap=True))
365
+ if value.unknown_events:
366
+ add(label(f"{value.unknown_events:,} events are of activities the model does not have ("
367
+ + ", ".join(f"{a}: {n:,}" for a, n in value.unknown_totals().most_common(8))
368
+ + "). Each counts one missing and one remaining token.", "muted", wrap=True))
365
369
  elif isinstance(value, AlignmentResult):
366
370
  from ..studio.alignment_view import AlignmentList, legend_items, projection
367
371
  from ..studio.widgets import Legend, LegendSwatch
@@ -42,9 +42,13 @@ CAPTIONS = {
42
42
  }
43
43
 
44
44
  #: Status of a task -> (chip text, status colour key).
45
+ #: The answer was shown before it was given: right now, but it does not count.
46
+ SHOWN = "shown"
47
+
45
48
  STATUS = {
46
49
  CORRECT: ("Correct", "good"), PARTIAL: ("Almost", "warning"),
47
50
  INCORRECT: ("Not yet", "critical"), "done": ("Done", "good"),
51
+ SHOWN: ("Answer shown", "muted"),
48
52
  }
49
53
 
50
54
 
@@ -771,6 +775,9 @@ class TaskCard(QFrame):
771
775
  self.status: str | None = None
772
776
  #: The share of the block's points the last check gave (for partial answers).
773
777
  self.share: float = 0.0
778
+ #: *Show answer* was pressed before the answer was right: a right answer
779
+ #: after that is the shown one typed in, so it is not marked Correct.
780
+ self.revealed: bool = False
774
781
  #: In an exam there is no hint, no answer, and Check only says if it is right.
775
782
  self.exam = exam
776
783
  layout = QVBoxLayout(self)
@@ -838,7 +845,7 @@ class TaskCard(QFrame):
838
845
  # -- feedback ------------------------------------------------------------------------
839
846
  def _edited(self) -> None:
840
847
  # Feedback is about the answer as it was: it goes when you change it.
841
- if self.status in (CORRECT, PARTIAL, INCORRECT) or not self.feedback.isHidden():
848
+ if self.status in (CORRECT, PARTIAL, INCORRECT, SHOWN) or not self.feedback.isHidden():
842
849
  self.set_status(None)
843
850
  self.feedback.setHidden(True)
844
851
  self._clear_detail()
@@ -876,8 +883,12 @@ class TaskCard(QFrame):
876
883
  status = result.status
877
884
  self.share = float(getattr(result, "share", 0.0) or 0.0)
878
885
  icon = {CORRECT: "✓", PARTIAL: "◐", INCORRECT: "✕"}.get(status, "")
879
- self.feedback.setText(f"<b>{icon}</b>&nbsp; {escape(result.message)}" if icon
880
- else escape(result.message))
886
+ message = escape(result.message)
887
+ if self.revealed and status in (CORRECT, PARTIAL):
888
+ # Still told whether it is right, but it earns nothing and is not "done".
889
+ status, self.share = SHOWN, 0.0
890
+ message += " <i>The answer was shown first, so this does not count.</i>"
891
+ self.feedback.setText(f"<b>{icon}</b>&nbsp; {message}" if icon else message)
881
892
  self.feedback.setProperty("state", STATUS.get(status, ("", ""))[1])
882
893
  self._repolish(self.feedback)
883
894
  self.feedback.setHidden(False)
@@ -912,8 +923,17 @@ class TaskCard(QFrame):
912
923
  self.solution_label.body.set_markdown(text)
913
924
  self.solution_label.setHidden(False)
914
925
  self.answer_button.setHidden(True)
915
- if self.task.type == "open" and self.status != "done":
916
- self.assess_row.setHidden(False)
926
+ if self.task.type == "open":
927
+ if self.status != "done":
928
+ self.assess_row.setHidden(False)
929
+ elif self.status not in (CORRECT, "done") and not self.revealed:
930
+ # An answer that was already right stays right; anything else was not
931
+ # answered before the answer was seen. (An open answer is judged by
932
+ # yourself against the model answer, so showing it is the point.)
933
+ self.revealed = True
934
+ if self.status == PARTIAL:
935
+ self.set_status(None)
936
+ self.changed.emit(self.task)
917
937
 
918
938
  def assess(self, had_it: bool) -> None:
919
939
  self.assess_row.setHidden(True)
@@ -59,7 +59,7 @@ from ...learn.checks import check as check_answer
59
59
  from ...learn.checks import expected, footprint_of, model_answer_text
60
60
  from ...learn.exam import ExamState, marks_csv
61
61
  from ...learn.pack import Exercise, Pack, load_pack
62
- from .answer_boxes import TaskCard, debounce, make_editor
62
+ from .answer_boxes import SHOWN, TaskCard, debounce, make_editor
63
63
  from .concealment import Concealment
64
64
  from ..studio.markdown_view import MarkdownLabel
65
65
  from ..studio.widgets import SegmentedControl, button, hbox, label, scroll
@@ -802,7 +802,7 @@ class ExerciseView(QWidget):
802
802
  self._workflow_timer = debounce(self, WORKFLOW_SAVE_DELAY, self._save_workflow)
803
803
  self._workflow_timer.start()
804
804
  card = self.cards.get(task.id)
805
- if card is not None and card.status in (CORRECT, PARTIAL, INCORRECT):
805
+ if card is not None and card.status in (CORRECT, PARTIAL, INCORRECT, SHOWN):
806
806
  card.editor.changed.emit() # the old verdict no longer applies
807
807
 
808
808
  def _workflow_saved(self, task) -> None:
@@ -839,7 +839,7 @@ class ExerciseView(QWidget):
839
839
  self._net_timers[task.id] = timer
840
840
  timer.start()
841
841
  card = self.cards.get(task.id)
842
- if card is not None and card.status in (CORRECT, PARTIAL, INCORRECT):
842
+ if card is not None and card.status in (CORRECT, PARTIAL, INCORRECT, SHOWN):
843
843
  card.editor.changed.emit() # the old verdict no longer applies
844
844
 
845
845
  def _save_net(self, page, target: Path) -> None:
@@ -1004,6 +1004,7 @@ class ExerciseView(QWidget):
1004
1004
  saved = self.progress.get(card.task.id, {})
1005
1005
  if card.task.type not in ("net", "workflow") and "answer" in saved:
1006
1006
  card.editor.set_value(saved["answer"])
1007
+ card.revealed = bool(saved.get("shown"))
1007
1008
  status = saved.get("status")
1008
1009
  if status:
1009
1010
  card.set_status(status)
@@ -1018,6 +1019,10 @@ class ExerciseView(QWidget):
1018
1019
  entry["status"] = card.status
1019
1020
  else:
1020
1021
  entry.pop("status", None)
1022
+ if card.revealed:
1023
+ entry["shown"] = True
1024
+ else:
1025
+ entry.pop("shown", None)
1021
1026
  if card.status == PARTIAL and card.share:
1022
1027
  entry["share"] = round(card.share, 4)
1023
1028
  else:
@@ -823,7 +823,11 @@ class CpnPage(QWidget):
823
823
  def _update_sim_status(self) -> None:
824
824
  if not self.simulating:
825
825
  problems = len(self.net.errors)
826
- self.sim_status.setText("✓ compiles" if not problems else
826
+ ignored = len(self.net.warnings)
827
+ self.sim_status.setText((f"⚠ compiles, but {ignored} priorit"
828
+ f"{'y or code segment is' if ignored == 1 else 'ies or code segments are'}"
829
+ " not used — see the Problems tab" if ignored else "✓ compiles")
830
+ if not problems else
827
831
  f"⚠ {problems} problem{'s' * (problems != 1)} — see "
828
832
  "the Problems tab")
829
833
  return
@@ -909,18 +913,19 @@ class CpnPage(QWidget):
909
913
 
910
914
  def _fill_problems(self) -> None:
911
915
  errors = list(self.net.errors)
912
- count = len(errors)
916
+ warnings = list(self.net.warnings)
917
+ count = len(errors) + len(warnings)
913
918
  self.inspector_tabs.buttons[2].setText(f"Problems ({count})" if count else "Problems")
914
919
  card = self.problems_card
915
920
  card.clear()
916
921
  if not errors:
917
922
  card.add(Verdict("Model compiles", "good", "Every declaration and inscription "
918
923
  "parsed. The model is ready to simulate."))
919
- return
920
- for issue in errors[:200]:
924
+ for issue in (errors + warnings)[:200]:
921
925
  row = QWidget()
922
926
  title = f"{issue.element_name or 'Declarations'} — {issue.field_name}"
923
- layout = hbox(Verdict(title, "critical", issue.message), spacing=6)
927
+ layout = hbox(Verdict(title, "critical" if issue in errors else "warning",
928
+ issue.message), spacing=6)
924
929
  if issue.element_id:
925
930
  layout.addWidget(button("Show", lambda _=False, i=issue.element_id:
926
931
  self.reveal_element(i), kind="ghost"), 0, Qt.AlignTop)
@@ -237,6 +237,8 @@ def properties(props: PropertyReport, key: str) -> list[str]:
237
237
  m = marking(net, graph.states[state])
238
238
  return [f"{m0} {firing(net, graph.path_to(state))} {m}",
239
239
  f"\\forall t \\in T \\colon t \\text{{ is not enabled in }} {m}"]
240
+ if props.deadlock_free is None:
241
+ return ["\\text{not decided: the state space is unbounded or too large}"]
240
242
  return [f"\\forall M \\in {reach} \\; \\exists t \\in T \\colon t "
241
243
  "\\text{ enabled in } M"]
242
244
  if key == "dead_transition":
@@ -627,7 +627,9 @@ class ModelPage(ConcealsResults, QWidget):
627
627
  f"log-level {alignments.log_fitness:.3f}")
628
628
  tiles["tbr"].set(f"{replay.fitness:.3f}",
629
629
  f"m {replay.missing:,} · r {replay.remaining:,} · "
630
- f"c {replay.consumed:,} · p {replay.produced:,}")
630
+ f"c {replay.consumed:,} · p {replay.produced:,}"
631
+ + (f" · {replay.unknown_events:,} events not in the model"
632
+ if replay.unknown_events else ""))
631
633
  tiles["precision"].set(f"{prec:.3f}", "escaping edges (ETC)")
632
634
  tiles["generalisation"].set(f"{gen:.3f}", "token-based")
633
635
  tiles["simplicity"].set(f"{simp:.3f}", "arc degree")
@@ -1020,6 +1020,7 @@ def cpn_info(path: str) -> dict:
1020
1020
  compile problems."""
1021
1021
  net = _cpn(path)
1022
1022
  return {"name": net.name, "problems": [str(e) for e in net.errors],
1023
+ "not_implemented": [str(w) for w in net.warnings],
1023
1024
  "pages": [{"name": p.name, "places": len(p.places), "transitions": len(p.transitions),
1024
1025
  "arcs": len(p.arcs)} for p in net.pages],
1025
1026
  "colour_sets": {n: type(c).__name__ for n, c in sorted(net.declarations.colour_sets.items())},
@@ -1041,6 +1042,7 @@ def cpn_simulate(path: str, steps: int = 50, seed: int = 0) -> dict:
1041
1042
  return {"steps": taken, "asked": steps, "clock": simulator.clock, "dead": taken < steps,
1042
1043
  "log": [record.describe(net) for record in simulator.log][-200:],
1043
1044
  "final_marking": simulator.marking.describe(net),
1045
+ "not_implemented": [str(w) for w in net.warnings],
1044
1046
  "cite": citations("openprocess.sim"),
1045
1047
  "reproduce": f"openprocess simulate {_q(path)} --steps {steps} --seed {seed}"}
1046
1048
 
@@ -338,8 +338,18 @@ class PropertyReport:
338
338
  return self.bounded and self.bound <= 1
339
339
 
340
340
  @property
341
- def deadlock_free(self) -> bool:
342
- return not self.dead_markings
341
+ def deadlock_free(self) -> bool | None:
342
+ """False with a dead marking in hand (in a coverability graph too: a
343
+ dead node stands for reachable markings in which nothing is enabled).
344
+ None when no dead marking was found but the search cannot prove there
345
+ is none: a coverability graph's ω enables what a finite marking may
346
+ not, so it can hide a deadlock, and a truncated graph has unexplored
347
+ markings."""
348
+ if self.dead_markings:
349
+ return False
350
+ if self.graph.has_omega or self.graph.truncated:
351
+ return None
352
+ return True
343
353
 
344
354
  @property
345
355
  def terminating(self) -> bool | None:
@@ -375,8 +385,12 @@ class PropertyReport:
375
385
  else "some run can go on forever"
376
386
  + (" (the net is unbounded)" if self.graph.has_omega
377
387
  else ": the reachability graph has a cycle")))
378
- rows.append(("Deadlock-free", self.deadlock_free,
379
- "every reachable marking enables something" if self.deadlock_free
388
+ deadlock_free = self.deadlock_free
389
+ rows.append(("Deadlock-free", deadlock_free,
390
+ "every reachable marking enables something" if deadlock_free
391
+ else ("not decided: the net is unbounded, and the coverability graph can "
392
+ "hide a deadlock" if self.graph.has_omega
393
+ else "not decided (truncated state space)") if deadlock_free is None
380
394
  else f"{len(self.dead_markings)} dead marking(s), e.g. "
381
395
  + self.graph.states[self.dead_markings[0]].describe(net)
382
396
  + " via " + self.graph.describe_path(self.dead_markings[0])))
@@ -672,7 +686,7 @@ class ShortCircuitReport:
672
686
  return self.properties.safe
673
687
 
674
688
  @property
675
- def deadlock_free(self) -> bool:
689
+ def deadlock_free(self) -> bool | None:
676
690
  return self.properties.deadlock_free
677
691
 
678
692
  @property
@@ -119,6 +119,15 @@ class AlignmentResult:
119
119
  empty_trace_cost: int = 0
120
120
  #: What the reader should know: searches that were cut short, and why.
121
121
  notes: list[str] = field(default_factory=list)
122
+ #: False when the search for the model's shortest run gave up: the
123
+ #: empty-trace cost is then unknown (counted as 0), which makes every
124
+ #: fitness a lower bound. (Named as a working value so that a result's
125
+ #: fingerprint is what it was; ``notes`` says so when it is False.)
126
+ _empty_trace_complete: bool = True
127
+
128
+ @property
129
+ def empty_trace_complete(self) -> bool:
130
+ return self._empty_trace_complete
122
131
 
123
132
  @property
124
133
  def trace_count(self) -> int:
@@ -129,16 +138,25 @@ class AlignmentResult:
129
138
  """The variants whose optimal alignment was not found (state limit)."""
130
139
  return [a for a in self.alignments if not a.complete]
131
140
 
141
+ @property
142
+ def exact(self) -> bool:
143
+ """Was an optimal alignment found for every variant and for the empty
144
+ trace? If not, the fitness values are lower bounds, not the fitness."""
145
+ return self.empty_trace_complete and not self.incomplete
146
+
132
147
  @property
133
148
  def average_fitness(self) -> float:
134
- """Mean of the per-trace fitness values (weighted by frequency)."""
149
+ """Mean of the per-trace fitness values (weighted by frequency). A
150
+ variant whose search gave up counts as 0: a lower bound (see :attr:`exact`)."""
135
151
  total = self.trace_count
136
152
  return sum(a.fitness * a.count for a in self.alignments) / total if total else 1.0
137
153
 
138
154
  @property
139
155
  def log_fitness(self) -> float:
140
- """1 − total cost / total worst-case cost."""
141
- cost = sum(a.cost * a.count for a in self.alignments)
156
+ """1 − total cost / total worst-case cost. A variant whose search gave
157
+ up counts its worst case, as it counts 0 in :attr:`average_fitness`."""
158
+ cost = sum((a.cost if a.complete else len(a.trace) + self.empty_trace_cost) * a.count
159
+ for a in self.alignments)
142
160
  worst = sum((len(a.trace) + self.empty_trace_cost) * a.count for a in self.alignments)
143
161
  return 1 - cost / worst if worst else 1.0
144
162
 
@@ -307,7 +325,7 @@ def align_log(net: PetriNet, log: SimpleLog, max_states: int = MAX_STATES,
307
325
  from .. import parallel
308
326
  compiled = _CompiledNet(net)
309
327
  empty = align_trace(net, (), 1, max_states, compiled)
310
- result = AlignmentResult(net, empty_trace_cost=empty.cost)
328
+ result = AlignmentResult(net, empty_trace_cost=empty.cost, _empty_trace_complete=empty.complete)
311
329
  variants = sorted(log.items(), key=lambda item: (-item[1], item[0]))
312
330
  count = parallel.workers() if workers is None else workers
313
331
  if parallel.worth_it(variants, count, "align"):
@@ -318,7 +336,9 @@ def align_log(net: PetriNet, log: SimpleLog, max_states: int = MAX_STATES,
318
336
  alignment = searched[number - 1] if searched is not None else align_trace(net, trace, count, max_states,
319
337
  compiled)
320
338
  worst = len(trace) + empty.cost
321
- alignment.fitness = 1 - alignment.cost / worst if worst else 1.0
339
+ # max: with the empty-trace cost unknown (counted as 0) the worst case is
340
+ # too small, and the value must stay a lower bound of the fitness.
341
+ alignment.fitness = max(0.0, 1 - alignment.cost / worst) if worst else 1.0
322
342
  if not alignment.complete:
323
343
  alignment.fitness = 0.0
324
344
  result.alignments.append(alignment)
@@ -340,7 +360,9 @@ def align_log(net: PetriNet, log: SimpleLog, max_states: int = MAX_STATES,
340
360
  f"variants ({cases:,} cases): the search stopped at {max_states:,} states. Their "
341
361
  f"cost is unknown and their fitness is counted as 0."
342
362
  if result.incomplete else
343
- f"The search for the model's shortest run stopped at {max_states:,} states.")
363
+ f"The search for the model's shortest run stopped at {max_states:,} states, so the "
364
+ f"worst-case cost every fitness is measured against is too small: each fitness "
365
+ f"is a lower bound.")
344
366
  reason = _unbounded_reason(net)
345
367
  if reason:
346
368
  result.notes.append(reason)
@@ -34,6 +34,19 @@ through silent moves, then the one missing the fewest tokens. These are
34
34
  heuristics -- token replay is fast but not always optimal, which is why
35
35
  alignments exist (see :mod:`alignments`).
36
36
 
37
+ Activities the model does not have
38
+ ----------------------------------
39
+ The book replays logs whose activities all occur in the model. A real log
40
+ often has more. Skipping such an event would count it as if it fitted (a
41
+ trace of nothing but unknown activities would score 1), so it is counted as
42
+ what it is, something the log did that the model cannot do: the event is
43
+ replayed as a transition of its own with one input and one output place that
44
+ nothing else touches. Its input token is missing and its output token
45
+ remains, so each such event adds 1 to each of p, c, m and r. This is
46
+ OpenProcess's convention, not a published one; ``unknown_events`` on the
47
+ result says how many events it applied to, and the tokens are counted under
48
+ the activity (they belong to no place of the model).
49
+
37
50
  Diagnostics
38
51
  -----------
39
52
  Per place we keep the total missing and remaining tokens, and per transition
@@ -59,7 +72,9 @@ class TraceReplay:
59
72
  consumed: int
60
73
  missing: int
61
74
  remaining: int
62
- #: activities that do not occur in the model at all
75
+ #: the activity of every event the model has no transition for, in trace
76
+ #: order; each counts one missing and one remaining token (see the module's
77
+ #: "Activities the model does not have")
63
78
  unknown_activities: list[str] = field(default_factory=list)
64
79
  #: indices of events whose transition was not enabled
65
80
  problem_events: list[int] = field(default_factory=list)
@@ -120,6 +135,19 @@ class ReplayResult:
120
135
  def trace_count(self) -> int:
121
136
  return sum(t.count for t in self.traces)
122
137
 
138
+ @property
139
+ def unknown_events(self) -> int:
140
+ """Events whose activity the model does not have, frequency-weighted."""
141
+ return sum(len(t.unknown_activities) * t.count for t in self.traces)
142
+
143
+ def unknown_totals(self) -> Counter:
144
+ """Those events per activity, frequency-weighted."""
145
+ totals: Counter = Counter()
146
+ for t in self.traces:
147
+ for activity in t.unknown_activities:
148
+ totals[activity] += t.count
149
+ return totals
150
+
123
151
  def place_totals(self) -> tuple[Counter, Counter]:
124
152
  """(missing, remaining) per place, frequency-weighted."""
125
153
  missing, remaining = Counter(), Counter()
@@ -368,11 +396,17 @@ def replay_trace(net: PetriNet, trace: tuple[str, ...], count: int = 1,
368
396
  result.fired[transition] += 1
369
397
  return current - pre + post
370
398
 
399
+ unknown_remaining = 0
371
400
  for index, activity in enumerate(trace):
372
401
  candidates = by_label.get(activity)
373
402
  if not candidates:
403
+ # No transition for it: one token missing to do it, one left behind.
374
404
  result.unknown_activities.append(activity)
375
405
  result.problem_events.append(index)
406
+ result.produced += 1
407
+ result.consumed += 1
408
+ result.missing += 1
409
+ unknown_remaining += 1
376
410
  continue
377
411
  chosen: str | None = next((t for t in candidates if net.is_enabled(marking, t)), None)
378
412
  if chosen is None:
@@ -408,7 +442,7 @@ def replay_trace(net: PetriNet, trace: tuple[str, ...], count: int = 1,
408
442
  marking = marking + short
409
443
  result.consumed += final.total
410
444
  leftover = marking - final
411
- result.remaining = leftover.total
445
+ result.remaining = leftover.total + unknown_remaining
412
446
  result.remaining_by_place.update(dict(leftover.items()))
413
447
  return result
414
448
 
@@ -316,6 +316,10 @@ class CPNet:
316
316
  #: declarations. Recreated by :meth:`compile`.
317
317
  self.evaluator = Evaluator(seed=seed)
318
318
  self.errors: list[CompileIssue] = []
319
+ #: What the model says that OpenProcess does not do (a transition's
320
+ #: priority, its code segment): it still simulates, differently from
321
+ #: CPN Tools. Filled by :meth:`compile`.
322
+ self.warnings: list[CompileIssue] = []
319
323
  #: place id -> the id under which its tokens are stored in a Marking.
320
324
  #: Identity for ordinary places; the group representative for places in
321
325
  #: a fusion set (see :meth:`marking_key`).
@@ -360,6 +364,7 @@ class CPNet:
360
364
  empty list means the model is ready to simulate.
361
365
  """
362
366
  self.errors = []
367
+ self.warnings = []
363
368
  self.evaluator = Evaluator(seed=self.seed)
364
369
  self._build_fusion_map()
365
370
 
@@ -396,6 +401,20 @@ class CPNet:
396
401
  transition.guard_ast = parse_guard(guard)
397
402
  except CPNMLError as error:
398
403
  self._record(transition.id, transition.name, "guard", str(error))
404
+ priority = transition.priority_text.strip()
405
+ if priority and priority != "P_NORMAL":
406
+ self.warnings.append(CompileIssue(
407
+ transition.id, transition.name, "priority",
408
+ f"the priority {priority} is not used: priorities are not implemented, so "
409
+ "this transition can fire whenever it is enabled, also when CPN Tools "
410
+ "would let another one go first. Simulation and the state space can "
411
+ "differ from CPN Tools."))
412
+ if transition.code_text.strip():
413
+ self.warnings.append(CompileIssue(
414
+ transition.id, transition.name, "code segment",
415
+ "the code segment is not run: code segments are not implemented, so its "
416
+ "action does not happen and the variables it would set stay unbound or "
417
+ "are bound freely. Simulation and the state space can differ from CPN Tools."))
399
418
  delay = transition.time_text.strip().lstrip("@+").strip()
400
419
  if delay:
401
420
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.16.1
3
+ Version: 0.16.2
4
4
  Summary: Process mining, Petri nets and workflows in one open app: every algorithm visible, every result reproducible, every step teachable
5
5
  Author: Sam Struthers
6
6
  License: MIT
@@ -742,6 +742,9 @@ course).
742
742
 
743
743
  ## For developers
744
744
 
745
+ Want to report a wrong result, add an algorithm or fix something? See
746
+ [**CONTRIBUTING.md**](CONTRIBUTING.md).
747
+
745
748
  ```bash
746
749
  pytest -q # the whole suite, including GUI tests that run offscreen
747
750
  openprocess --help # command line: check, simulate, state space, mining
@@ -1089,7 +1092,10 @@ Plainly stated:
1089
1092
  each use its own copy of the page. Subpages used once, at any depth,
1090
1093
  simulate.
1091
1094
  2. **No CPN monitors or simulation reports**, and **no fairness
1092
- properties** in the state space report (it says so).
1095
+ properties** in the state space report (it says so). **Transition
1096
+ priorities and code segments are kept but not run**: a model that uses
1097
+ them simulates differently from CPN Tools, and the Problems tab and the
1098
+ state space report list where.
1093
1099
  3. **CPN ML is a large subset, not all of Standard ML**: no `exception`
1094
1100
  declarations or `handle`, no `datatype` or `structure` declarations, and
1095
1101
  characters are one-letter strings. See