openprocess 0.13.0__tar.gz → 0.13.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 (269) hide show
  1. {openprocess-0.13.0/openprocess.egg-info → openprocess-0.13.2}/PKG-INFO +15 -2
  2. {openprocess-0.13.0 → openprocess-0.13.2}/README.md +14 -1
  3. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/__init__.py +1 -1
  4. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/__init__.py +5 -0
  5. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/input.py +5 -9
  6. openprocess-0.13.2/openprocess/flow/loading.py +239 -0
  7. openprocess-0.13.2/openprocess/flow/parallel.py +117 -0
  8. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/studio.py +20 -1
  9. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/page.py +27 -3
  10. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/app.py +93 -12
  11. openprocess-0.13.2/openprocess/gui/studio/connect.py +212 -0
  12. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/connections.py +10 -0
  13. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/definition_view.py +12 -4
  14. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/documents.py +5 -1
  15. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mcp/connecting.md +15 -6
  16. openprocess-0.13.2/openprocess/mcp/install.py +273 -0
  17. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mcp/server.py +4 -1
  18. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/conformance/alignments.py +15 -3
  19. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/conformance/token_replay.py +12 -4
  20. openprocess-0.13.2/openprocess/mining/parallel.py +81 -0
  21. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/petrinet.py +13 -0
  22. openprocess-0.13.2/openprocess/parallel.py +374 -0
  23. {openprocess-0.13.0 → openprocess-0.13.2/openprocess.egg-info}/PKG-INFO +15 -2
  24. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess.egg-info/SOURCES.txt +9 -0
  25. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_flow.py +18 -0
  26. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_mcp.py +37 -0
  27. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_mcp_gui.py +31 -0
  28. openprocess-0.13.2/tests/test_parallel_and_loading.py +175 -0
  29. openprocess-0.13.2/tests/test_parallel_boxes.py +178 -0
  30. openprocess-0.13.2/tests/test_researcher_api.py +156 -0
  31. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_studio_gui.py +1 -1
  32. openprocess-0.13.2/tests/test_studio_lazy.py +70 -0
  33. openprocess-0.13.0/openprocess/mcp/install.py +0 -127
  34. {openprocess-0.13.0 → openprocess-0.13.2}/LICENSE +0 -0
  35. {openprocess-0.13.0 → openprocess-0.13.2}/cpnpy/__init__.py +0 -0
  36. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/analysis/__init__.py +0 -0
  37. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/analysis/state_space.py +0 -0
  38. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/analysis/state_space_process.py +0 -0
  39. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/citation.py +0 -0
  40. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/cli.py +0 -0
  41. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
  42. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
  43. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
  44. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
  45. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
  46. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
  47. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
  48. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
  49. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
  50. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
  51. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
  52. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
  53. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
  54. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
  55. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
  56. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
  57. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
  58. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
  59. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
  60. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/exercises/pack.md +0 -0
  61. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/box.py +0 -0
  62. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/__init__.py +0 -0
  63. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/check.py +0 -0
  64. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/compare.py +0 -0
  65. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/cpn.py +0 -0
  66. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/discover.py +0 -0
  67. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/filter.py +0 -0
  68. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/measure.py +0 -0
  69. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/output.py +0 -0
  70. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/predict.py +0 -0
  71. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/science.py +0 -0
  72. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/boxes/sweeps.py +0 -0
  73. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/changes.py +0 -0
  74. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/convert.py +0 -0
  75. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/datasets.py +0 -0
  76. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/derivation.py +0 -0
  77. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/explain.py +0 -0
  78. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/figures.py +0 -0
  79. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/history.py +0 -0
  80. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/library.py +0 -0
  81. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/provenance.py +0 -0
  82. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/record.py +0 -0
  83. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/report.py +0 -0
  84. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/reproducibility.py +0 -0
  85. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/runner.py +0 -0
  86. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/script.py +0 -0
  87. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/snapshots.py +0 -0
  88. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/sweep.py +0 -0
  89. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/types.py +0 -0
  90. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/flow/workflow.py +0 -0
  91. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/__init__.py +0 -0
  92. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/app.py +0 -0
  93. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/arc_editing.py +0 -0
  94. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/canvas.py +0 -0
  95. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/__init__.py +0 -0
  96. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/canvas.py +0 -0
  97. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/picker.py +0 -0
  98. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/runs.py +0 -0
  99. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/stepper.py +0 -0
  100. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/summary.py +0 -0
  101. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/templates.py +0 -0
  102. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/flow/viewers.py +0 -0
  103. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/items.py +0 -0
  104. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/learn/answer_boxes.py +0 -0
  105. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/learn/concealment.py +0 -0
  106. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/learn/mode.py +0 -0
  107. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/panning.py +0 -0
  108. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/resources/openprocess-icon.png +0 -0
  109. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/__init__.py +0 -0
  110. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/__main__.py +0 -0
  111. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/alignment_view.py +0 -0
  112. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/charts.py +0 -0
  113. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/cite.py +0 -0
  114. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/command_palette.py +0 -0
  115. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/compare_page.py +0 -0
  116. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/cpn_page.py +0 -0
  117. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/derivation_view.py +0 -0
  118. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/dotted_chart.py +0 -0
  119. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/file_dialogs.py +0 -0
  120. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/filter_dialog.py +0 -0
  121. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/graph_builders.py +0 -0
  122. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/graph_view.py +0 -0
  123. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/instances.py +0 -0
  124. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/loading.py +0 -0
  125. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/log_editor.py +0 -0
  126. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/log_page.py +0 -0
  127. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/markdown_view.py +0 -0
  128. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/mathtext.py +0 -0
  129. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/ml_highlighter.py +0 -0
  130. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/model_page.py +0 -0
  131. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/motion.py +0 -0
  132. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/net_comparison.py +0 -0
  133. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/notes_overlay.py +0 -0
  134. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/petri_page.py +0 -0
  135. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/provenance.py +0 -0
  136. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/regions_view.py +0 -0
  137. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/remote.py +0 -0
  138. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/review.py +0 -0
  139. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/sidebar.py +0 -0
  140. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/spaces.py +0 -0
  141. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/style.py +0 -0
  142. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/tool_icons.py +0 -0
  143. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/updates.py +0 -0
  144. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/widgets.py +0 -0
  145. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/workers.py +0 -0
  146. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/studio/workspace.py +0 -0
  147. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/theme.py +0 -0
  148. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/gui/tidy.py +0 -0
  149. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/io/__init__.py +0 -0
  150. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/io/cpn_reader.py +0 -0
  151. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/io/cpn_writer.py +0 -0
  152. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/__init__.py +0 -0
  153. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/answers.py +0 -0
  154. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/checks.py +0 -0
  155. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/computed.py +0 -0
  156. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/context.py +0 -0
  157. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/exam.py +0 -0
  158. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/exercise-packs.md +0 -0
  159. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/importer.py +0 -0
  160. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/notation.py +0 -0
  161. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/pack.py +0 -0
  162. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/learn/sheet.py +0 -0
  163. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mcp/__init__.py +0 -0
  164. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mcp/__main__.py +0 -0
  165. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mcp/api.py +0 -0
  166. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/__init__.py +0 -0
  167. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/analysis.py +0 -0
  168. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/bpmn.py +0 -0
  169. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/cases.py +0 -0
  170. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/columns.py +0 -0
  171. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/compare_nets.py +0 -0
  172. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/conformance/__init__.py +0 -0
  173. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/conformance/quality.py +0 -0
  174. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/csv_import.py +0 -0
  175. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/definitions.py +0 -0
  176. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/dfg.py +0 -0
  177. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/discovery/__init__.py +0 -0
  178. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/discovery/alpha.py +0 -0
  179. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/discovery/heuristics.py +0 -0
  180. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/discovery/inductive.py +0 -0
  181. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/discovery/state_regions.py +0 -0
  182. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/dotted.py +0 -0
  183. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/drawing.py +0 -0
  184. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/filtering.py +0 -0
  185. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/footprint.py +0 -0
  186. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/invariants.py +0 -0
  187. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/layout.py +0 -0
  188. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/log.py +0 -0
  189. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/ocel.py +0 -0
  190. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/playout.py +0 -0
  191. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/pm4py_bridge.py +0 -0
  192. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/pnml.py +0 -0
  193. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/processtree.py +0 -0
  194. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/regions.py +0 -0
  195. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/stats.py +0 -0
  196. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/structure.py +0 -0
  197. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/transition_system.py +0 -0
  198. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/mining/xes.py +0 -0
  199. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/__init__.py +0 -0
  200. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/ast_nodes.py +0 -0
  201. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/builtins.py +0 -0
  202. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/colorsets.py +0 -0
  203. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/errors.py +0 -0
  204. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/evaluator.py +0 -0
  205. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/lexer.py +0 -0
  206. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/multiset.py +0 -0
  207. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/parser.py +0 -0
  208. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/ml/values.py +0 -0
  209. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/model/__init__.py +0 -0
  210. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/model/declarations.py +0 -0
  211. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/model/examples.py +0 -0
  212. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/model/net.py +0 -0
  213. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/model/plain.py +0 -0
  214. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/packages.py +0 -0
  215. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/palette.py +0 -0
  216. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/references.py +0 -0
  217. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/sim/__init__.py +0 -0
  218. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/sim/binding.py +0 -0
  219. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/sim/export.py +0 -0
  220. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/sim/performance.py +0 -0
  221. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/sim/simulator.py +0 -0
  222. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/teaching/__init__.py +0 -0
  223. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/teaching/answers.py +0 -0
  224. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/teaching/checks.py +0 -0
  225. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/teaching/pack.py +0 -0
  226. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess/teaching/sheet.py +0 -0
  227. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess.egg-info/dependency_links.txt +0 -0
  228. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess.egg-info/entry_points.txt +0 -0
  229. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess.egg-info/requires.txt +0 -0
  230. {openprocess-0.13.0 → openprocess-0.13.2}/openprocess.egg-info/top_level.txt +0 -0
  231. {openprocess-0.13.0 → openprocess-0.13.2}/pyproject.toml +0 -0
  232. {openprocess-0.13.0 → openprocess-0.13.2}/setup.cfg +0 -0
  233. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_arc_editing.py +0 -0
  234. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_bpmn.py +0 -0
  235. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_citation.py +0 -0
  236. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_colorsets.py +0 -0
  237. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_columns.py +0 -0
  238. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_cpn_course_model.py +0 -0
  239. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_definitions.py +0 -0
  240. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_docs.py +0 -0
  241. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_exercises_gui.py +0 -0
  242. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_flow_gui.py +0 -0
  243. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_framework_contract.py +0 -0
  244. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_hierarchy.py +0 -0
  245. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_history.py +0 -0
  246. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_io.py +0 -0
  247. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_large_logs.py +0 -0
  248. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_learn.py +0 -0
  249. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_learn_gui.py +0 -0
  250. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_log_editing.py +0 -0
  251. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_measure.py +0 -0
  252. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_mining.py +0 -0
  253. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_ml.py +0 -0
  254. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_multiset.py +0 -0
  255. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_ocel.py +0 -0
  256. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_packages.py +0 -0
  257. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_performance.py +0 -0
  258. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_regions.py +0 -0
  259. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_rename.py +0 -0
  260. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_report.py +0 -0
  261. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_review_fixes.py +0 -0
  262. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_simulation.py +0 -0
  263. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_snapshots.py +0 -0
  264. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_state_space.py +0 -0
  265. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_teaching.py +0 -0
  266. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_tidy.py +0 -0
  267. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_updates.py +0 -0
  268. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_wf_structure.py +0 -0
  269. {openprocess-0.13.0 → openprocess-0.13.2}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.13.0
3
+ Version: 0.13.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
@@ -938,6 +938,17 @@ def last_two(log: EventLog, representation: str = "multiset") -> TransitionSyste
938
938
  ...
939
939
  ```
940
940
 
941
+ Work over many independent items uses every core with one call, and gives
942
+ exactly the result of a loop, notes on the *How* tab included:
943
+
944
+ ```python
945
+ rows = flow.parallel_map(fit_of, variants, shared=model) # fit_of(model, variant), at the top level
946
+ ```
947
+
948
+ See [Parallel work](docs/workflows.md#parallel-work) for the rules (each item
949
+ from its own inputs, `flow.item_seed` for randomness). OpenProcess's own
950
+ alignments and token replay run this way on large logs.
951
+
941
952
  `pip install -e ".[science]"` adds the pandas, NumPy, SciPy and matplotlib
942
953
  boxes (describe, bootstrap intervals, tests, plots).
943
954
 
@@ -1025,7 +1036,9 @@ the clone is older than the latest release. See
1025
1036
 
1026
1037
  OpenProcess comes with an **MCP server**, so Claude (Desktop or Code), Cursor
1027
1038
  or any other assistant that speaks the Model Context Protocol can use it the
1028
- way you do:
1039
+ way you do. In the app: **Help ▸ Connect an AI Assistant…**, then **Connect**
1040
+ next to your assistant, and restart it. No terminal needed: the connector is
1041
+ inside the app. From a pip install:
1029
1042
 
1030
1043
  ```bash
1031
1044
  pip install "openprocess[app,mcp]"
@@ -896,6 +896,17 @@ def last_two(log: EventLog, representation: str = "multiset") -> TransitionSyste
896
896
  ...
897
897
  ```
898
898
 
899
+ Work over many independent items uses every core with one call, and gives
900
+ exactly the result of a loop, notes on the *How* tab included:
901
+
902
+ ```python
903
+ rows = flow.parallel_map(fit_of, variants, shared=model) # fit_of(model, variant), at the top level
904
+ ```
905
+
906
+ See [Parallel work](docs/workflows.md#parallel-work) for the rules (each item
907
+ from its own inputs, `flow.item_seed` for randomness). OpenProcess's own
908
+ alignments and token replay run this way on large logs.
909
+
899
910
  `pip install -e ".[science]"` adds the pandas, NumPy, SciPy and matplotlib
900
911
  boxes (describe, bootstrap intervals, tests, plots).
901
912
 
@@ -983,7 +994,9 @@ the clone is older than the latest release. See
983
994
 
984
995
  OpenProcess comes with an **MCP server**, so Claude (Desktop or Code), Cursor
985
996
  or any other assistant that speaks the Model Context Protocol can use it the
986
- way you do:
997
+ way you do. In the app: **Help ▸ Connect an AI Assistant…**, then **Connect**
998
+ next to your assistant, and restart it. No terminal needed: the connector is
999
+ inside the app. From a pip install:
987
1000
 
988
1001
  ```bash
989
1002
  pip install "openprocess[app,mcp]"
@@ -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.13.0"
51
+ __version__ = "0.13.2"
52
52
 
53
53
  __all__ = [
54
54
  "Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
@@ -26,12 +26,16 @@ Python::
26
26
  print(run.value(fit).metrics)
27
27
  save(wf, "demo.cpnflow", run) # with the record that makes it reproducible
28
28
 
29
+ Work over many independent items (variants, cases, resamples) runs on all
30
+ cores with :func:`parallel_map`, with exactly the result of a loop.
31
+
29
32
  See ``docs/workflows.md``.
30
33
  """
31
34
 
32
35
  from .box import Box, BoxError, BoxSpec, Port, Setting, box
33
36
  from .explain import Explanation, current, note, show, steps
34
37
  from .library import Library, library_for, standard_library
38
+ from .parallel import ParallelError, item_seed, parallel_map
35
39
  from .record import check, differences, load, save
36
40
  from .runner import Cache, Result, Run, Runner
37
41
  from .sweep import Sweep
@@ -47,5 +51,6 @@ __all__ = [
47
51
  "Port", "Predictions", "Predictor", "ProcessTree", "Regions", "ReplayResult", "Result", "Run",
48
52
  "Runner", "Scores", "Setting", "SimpleLog", "Sweep", "Table", "Text", "TransitionSystem", "Workflow",
49
53
  "WorkflowError", "box", "check", "current", "differences", "library_for", "load", "note",
54
+ "ParallelError", "item_seed", "parallel_map",
50
55
  "register_type", "save", "show", "standard_library", "steps", "to_python", "workflow",
51
56
  ]
@@ -32,15 +32,11 @@ def open_log(file: Path) -> EventLog:
32
32
  file: the log file. Choose… picks it; a name alone means a file in the workflow's folder
33
33
  """
34
34
  path = _resolve(file)
35
- lower = path.name.lower()
36
- if lower.endswith(".csv"):
37
- from ...mining.csv_import import read_csv
38
- log = read_csv(path)
39
- elif lower.endswith(".txt"):
40
- log = EventLog.from_simple_log(parse_simple_log(path.read_text(encoding="utf-8")), path.stem)
41
- else:
42
- from ...mining.xes import read_xes
43
- log = read_xes(path)
35
+ # Read once for everyone: the app's log page and other workflows reading
36
+ # the same file share it, and in the app a large log is read in a process
37
+ # of its own (openprocess.flow.loading).
38
+ from ..loading import read_log
39
+ log = read_log(path)
44
40
  flow.note(f"{path.name}: {len(log)} cases, {log.event_count} events, "
45
41
  f"classifier {log.default_classifier().name}")
46
42
  return log
@@ -0,0 +1,239 @@
1
+ """Reading event logs once, and large ones in a process of their own.
2
+
3
+ Why
4
+ ---
5
+ Reading a large XES file is pure Python. In a thread it holds Python's
6
+ interpreter lock most of the time, and the window, which needs the lock for
7
+ every widget it builds and every repaint, waits its turn thousands of times:
8
+ the app seems frozen although the read is "in the background". The BPI
9
+ Challenge 2019 log (728 MB) froze OpenProcess for over half a minute at
10
+ launch, twice over, because the log page and a workflow reading the same
11
+ file each parsed it.
12
+
13
+ What this module does
14
+ ---------------------
15
+ * :func:`read_log` reads a file **once** per content: a second request for
16
+ the same file (same path, size and modification time) gets the same
17
+ :class:`~openprocess.mining.log.EventLog` while anyone still holds it, and
18
+ a request made while it is being read waits for that read instead of
19
+ starting another (the log page and the workflow's *Open log* box share one).
20
+ * With :data:`use_processes` set (the app sets it), a log of
21
+ :data:`PROCESS_FROM` bytes or more is read by a **worker process**, which
22
+ has its own interpreter lock, so the window stays responsive. The worker
23
+ also hashes the file and fingerprints the log, the two things the
24
+ reproducibility badge needs, so the app does not redo them on the window's
25
+ thread. The log comes back pickled: a columnar log is a few arrays and
26
+ string tables, which unpickle in a fraction of a second.
27
+ * Scripts, notebooks and ``openprocess run`` read in-process as before
28
+ (``use_processes`` is False): no process to start, the same result.
29
+
30
+ Logs are read-only by convention (a box never changes its inputs, §9 of
31
+ ``docs/framework.md``; filters make new logs), which is what makes sharing
32
+ one object safe.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import os
38
+ import pickle
39
+ import subprocess
40
+ import sys
41
+ import tempfile
42
+ import threading
43
+ import weakref
44
+ from concurrent.futures import Future
45
+ from pathlib import Path
46
+ from typing import Callable
47
+
48
+ from ..mining.log import EventLog
49
+
50
+ #: Read large logs in a worker process (the app turns this on; scripts do not need it).
51
+ use_processes = False
52
+ #: From this size a log is read by a worker process (when :data:`use_processes`).
53
+ PROCESS_FROM = 20 * 1024 * 1024
54
+ #: First argument that makes a standalone app run as a log-reading worker.
55
+ WORKER_FLAG = "--openprocess-log-worker"
56
+
57
+ _lock = threading.Lock()
58
+ _cache: "weakref.WeakValueDictionary[tuple, EventLog]" = weakref.WeakValueDictionary()
59
+ _reading: dict[tuple, Future] = {}
60
+
61
+
62
+ class Cancelled(Exception):
63
+ """The read was stopped (the loading page's *Cancel*)."""
64
+
65
+
66
+ def file_key(path: str | Path) -> tuple:
67
+ """What identifies a file's content here: its resolved path, size and
68
+ modification time."""
69
+ resolved = Path(path).resolve()
70
+ stat = resolved.stat()
71
+ return (str(resolved), stat.st_size, stat.st_mtime_ns)
72
+
73
+
74
+ def cached(path: str | Path) -> EventLog | None:
75
+ """The log already read from this file (unchanged since), or None."""
76
+ try:
77
+ return _cache.get(file_key(path))
78
+ except OSError:
79
+ return None
80
+
81
+
82
+ def read_log(path: str | Path, progress: Callable | None = None) -> EventLog:
83
+ """The event log in ``path`` (``.xes``, ``.xes.gz``, ``.csv``, ``.txt`` in course
84
+ notation), read once for everyone who asks while it is in use.
85
+
86
+ ``progress(cases, share)`` is called while reading, as by
87
+ :func:`openprocess.mining.xes.read_xes`; it may raise to stop the read
88
+ (then :class:`Cancelled` or that exception comes out, for this caller;
89
+ another caller waiting on the same read gets it too)."""
90
+ key = file_key(path)
91
+ with _lock:
92
+ log = _cache.get(key)
93
+ if log is not None:
94
+ return log
95
+ pending = _reading.get(key)
96
+ if pending is None:
97
+ pending = _reading[key] = Future()
98
+ mine = True
99
+ else:
100
+ mine = False
101
+ if not mine:
102
+ return pending.result() # waits without holding the interpreter lock
103
+ try:
104
+ log = _read(Path(path), key, progress)
105
+ except BaseException as error:
106
+ with _lock:
107
+ _reading.pop(key, None)
108
+ pending.set_exception(error)
109
+ raise
110
+ with _lock:
111
+ _reading.pop(key, None)
112
+ _cache[key] = log
113
+ pending.set_result(log)
114
+ return log
115
+
116
+
117
+ def _read(path: Path, key: tuple, progress) -> EventLog:
118
+ lower = path.name.lower()
119
+ if lower.endswith(".csv"):
120
+ from ..mining.csv_import import read_csv
121
+ return read_csv(path)
122
+ if lower.endswith(".txt"):
123
+ from ..mining.log import parse_simple_log
124
+ return EventLog.from_simple_log(parse_simple_log(path.read_text(encoding="utf-8")), path.stem)
125
+ if use_processes and key[1] >= PROCESS_FROM:
126
+ return _read_in_process(path, progress)
127
+ from ..mining.xes import read_xes
128
+ return read_xes(path, progress=progress)
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # The worker process
133
+ # ---------------------------------------------------------------------------
134
+ def worker_command(*arguments: str) -> list[str]:
135
+ """How to start a worker: this module run by the same Python, or the
136
+ standalone app run again with :data:`WORKER_FLAG`."""
137
+ if getattr(sys, "frozen", False):
138
+ return [sys.executable, WORKER_FLAG, *arguments]
139
+ return [sys.executable, "-m", "openprocess.flow.loading", *arguments]
140
+
141
+
142
+ def _read_in_process(path: Path, progress) -> EventLog:
143
+ from . import record
144
+ with tempfile.TemporaryDirectory(prefix="openprocess-log-", ignore_cleanup_errors=True) as folder:
145
+ output = Path(folder) / "log.pickle"
146
+ environment = dict(os.environ, PYTHONIOENCODING="utf-8")
147
+ process = subprocess.Popen(worker_command(str(path), str(output)), stdin=subprocess.PIPE,
148
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
149
+ encoding="utf-8", env=environment,
150
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
151
+ error_lines: list[str] = []
152
+
153
+ def drain_errors() -> None:
154
+ for line in process.stderr:
155
+ error_lines.append(line)
156
+ threading.Thread(target=drain_errors, daemon=True).start()
157
+ outcome = None
158
+ try:
159
+ for line in process.stdout:
160
+ words = line.split()
161
+ if not words:
162
+ continue
163
+ if words[0] == "progress" and progress is not None and len(words) >= 3:
164
+ share = None if words[2] == "-" else float(words[2])
165
+ progress(int(words[1]), share) # may raise: Cancel
166
+ elif words[0] in ("done", "error"):
167
+ outcome = line.strip()
168
+ break
169
+ except BaseException:
170
+ process.kill()
171
+ process.wait()
172
+ raise
173
+ process.wait()
174
+ if outcome is None or outcome.startswith("error"):
175
+ detail = outcome[6:] if outcome else ("".join(error_lines[-8:]).strip() or
176
+ f"the reader stopped (exit {process.returncode})")
177
+ raise ValueError(detail)
178
+ with open(output, "rb") as handle:
179
+ log, file_hash, digest, summary = pickle.load(handle)
180
+ # What the worker worked out, so the badge does not redo it on the window's thread.
181
+ stat = path.resolve().stat()
182
+ record._file_hashes[(str(path.resolve()), stat.st_size, stat.st_mtime_ns)] = file_hash
183
+ record._remember(log, digest)
184
+ log.source_path = str(path)
185
+ # The overview's summary under the log's default classifier, worked out by the
186
+ # same summarise() in the worker: the log page shows it without computing it.
187
+ log.precomputed_summaries = {summary_key(log.default_classifier()): summary}
188
+ return log
189
+
190
+
191
+ def summary_key(classifier) -> tuple:
192
+ """How a precomputed summary is found again: the classifier's identity."""
193
+ return (classifier.name, tuple(getattr(classifier, "keys", ()) or ()),
194
+ tuple(sorted(getattr(classifier, "lifecycles", None) or ())))
195
+
196
+
197
+ def _child(path: str, output: str) -> int:
198
+ """The worker: read the log, hash the file, fingerprint the log and
199
+ summarise it (the overview), pickle all four to ``output``; ``progress <cases> <share>`` lines while reading,
200
+ then ``done`` or ``error <message>``. Stops when the parent goes away."""
201
+ for stream in (sys.stdout, sys.stdin):
202
+ if hasattr(stream, "reconfigure"):
203
+ stream.reconfigure(encoding="utf-8")
204
+
205
+ def watch_parent() -> None:
206
+ for _line in sys.stdin:
207
+ pass
208
+ os._exit(1) # stdin closed: the parent is gone or cancelled
209
+ threading.Thread(target=watch_parent, daemon=True).start()
210
+
211
+ def report(cases: int, share) -> None:
212
+ print(f"progress {cases} {'-' if share is None else f'{share:.4f}'}", flush=True)
213
+ try:
214
+ from ..mining.xes import read_xes
215
+ from .record import fingerprint, sha256_file
216
+ log = read_xes(path, progress=report)
217
+ print(f"progress {len(log)} 1.0", flush=True)
218
+ from ..mining.stats import summarise
219
+ payload = (log, sha256_file(path), fingerprint(log), summarise(log, log.default_classifier()))
220
+ with open(output, "wb") as handle:
221
+ pickle.dump(payload, handle, protocol=pickle.HIGHEST_PROTOCOL)
222
+ except Exception as error: # noqa: BLE001 - say what went wrong, in one line
223
+ print("error " + f"{type(error).__name__}: {error}".replace("\n", " "), flush=True)
224
+ return 1
225
+ print("done", flush=True)
226
+ return 0
227
+
228
+
229
+ def run_worker(arguments: list[str]) -> int:
230
+ """The standalone app's entry for :data:`WORKER_FLAG` (``[PATH, OUT]``)."""
231
+ return _child(arguments[0], arguments[1])
232
+
233
+
234
+ if __name__ == "__main__": # pragma: no cover - the worker process
235
+ raise SystemExit(_child(sys.argv[1], sys.argv[2]))
236
+
237
+
238
+ __all__ = ["Cancelled", "PROCESS_FROM", "WORKER_FLAG", "cached", "file_key", "read_log", "run_worker",
239
+ "use_processes", "worker_command"]
@@ -0,0 +1,117 @@
1
+ """Parallel work in a box: ``flow.parallel_map``.
2
+
3
+ A box whose work is the same computation over many independent items (the
4
+ variants of a log, the cases, the settings of a sweep inside one box, the
5
+ resamples of a bootstrap) can spread them over the machine's cores::
6
+
7
+ from openprocess import flow
8
+ from openprocess.flow import box, EventLog, PetriNet, Table
9
+
10
+
11
+ def score(model: PetriNet, variant) -> float: # top level: workers find it by name
12
+ trace, count = variant
13
+ ...
14
+ flow.note(f"{trace}: ...") # still reaches the How tab
15
+ return value
16
+
17
+
18
+ @box(group="Check")
19
+ def my_check(model: PetriNet, log: EventLog) -> Table:
20
+ variants = sorted(log.simple_log().items())
21
+ values = flow.parallel_map(score, variants, shared=model)
22
+ return Table("Scores", ["variant", "score"], [[v, s] for (v, _n), s in zip(variants, values)])
23
+
24
+ What it promises: the result is ``[score(model, v) for v in variants]``, the
25
+ same values in the same order, whatever the number of workers -- so the
26
+ workflow's record and ``openprocess run --check`` see the same numbers -- and
27
+ the notes, steps and values the item function reports appear on the box's
28
+ *How* tab in item order, as if the loop had run here. What it asks of the
29
+ function (``docs/workflows.md``, *Parallel work*): define it at the top level
30
+ of a module (a folder's ``boxes/`` file is fine); compute each item from the
31
+ item and ``shared`` alone; for randomness, seed per item with
32
+ :func:`item_seed`.
33
+
34
+ The machinery is :mod:`openprocess.parallel` (worker processes, batches,
35
+ ``OPENPROCESS_WORKERS``); this module adds the *How* tab.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ from typing import Any, Callable, Iterable
41
+
42
+ from .. import parallel as _core
43
+ from ..parallel import ParallelError, item_seed
44
+ from .explain import Explanation, current, explaining
45
+
46
+
47
+ def parallel_map(function: Callable, items: Iterable, *, shared: Any = None, setup: Callable | None = None,
48
+ workers: int | None = None, progress: Callable[[int, int], None] | None = None,
49
+ cost: Callable[[Any], float] | None = None) -> list:
50
+ """``[function(item) for item in items]`` on the machine's cores, in the
51
+ items' order; with ``shared`` (or ``setup``), ``function(context, item)``.
52
+
53
+ ``shared``: data every item needs (a model, a log), sent to each worker
54
+ once. ``setup(shared)``: run once per worker, its result is the
55
+ ``context`` (a compiled model, a cache). ``workers``: how many processes
56
+ (default: the cores less one, at most 8; ``1`` runs the loop here).
57
+ ``progress(done, total)``: called as items finish. ``cost(item)``: an
58
+ estimate of an item's cost, so the expensive ones start first.
59
+
60
+ Inside a running box, what ``function`` reports with :func:`flow.note`,
61
+ :func:`flow.steps` and :func:`flow.show` is added to the box's *How* tab
62
+ in item order. An item that fails raises :class:`ParallelError` with the
63
+ worker's traceback."""
64
+ items = list(items)
65
+ explanation = current()
66
+ if explanation is None or _core.reference(function) is None:
67
+ # Outside a box there is nothing to collect; a function the workers
68
+ # cannot find by name runs here, where its notes reach the tab directly.
69
+ return _core.map_items(function, items, shared=shared, setup=setup, workers=workers, progress=progress,
70
+ cost=cost)
71
+ setup_ref = _core.reference(setup) if setup is not None else None
72
+ if setup is not None and setup_ref is None:
73
+ return _core.map_items(function, items, shared=shared, setup=setup, workers=1, progress=progress)
74
+ package = (_core.reference(function), setup_ref, shared, shared is not None or setup is not None)
75
+ outcomes = _core.map_items(_explained, items, shared=package, setup=_explained_setup, workers=workers,
76
+ progress=progress, cost=cost)
77
+ values = []
78
+ for value, entries in outcomes:
79
+ _replay(explanation, entries)
80
+ values.append(value)
81
+ return values
82
+
83
+
84
+ def _explained_setup(package):
85
+ """In a worker: the user's function and its context."""
86
+ function_ref, setup_ref, shared, with_context = package
87
+ function = _core.resolve(function_ref)
88
+ context = _core.resolve(setup_ref)(shared) if setup_ref is not None else shared
89
+ return function, context, with_context
90
+
91
+
92
+ def _explained(prepared, item):
93
+ """In a worker: one item, with what it reported."""
94
+ function, context, with_context = prepared
95
+ with explaining() as explanation:
96
+ value = function(context, item) if with_context else function(item)
97
+ return value, explanation.entries
98
+
99
+
100
+ def _replay(explanation: Explanation, entries: list) -> None:
101
+ """Add a worker's report to the box's, as if it had been made here."""
102
+ for entry in entries:
103
+ explanation.entries.append(entry)
104
+ if entry[0] == "note":
105
+ explanation.notes.append(entry[1])
106
+ elif entry[0] == "steps":
107
+ explanation.steps.extend(entry[1])
108
+ elif entry[0] == "show":
109
+ explanation.shows.append((entry[1], entry[2]))
110
+
111
+
112
+ def workers() -> int:
113
+ """How many worker processes :func:`parallel_map` uses by default."""
114
+ return _core.workers()
115
+
116
+
117
+ __all__ = ["ParallelError", "item_seed", "parallel_map", "workers"]
@@ -44,10 +44,29 @@ def socket_path() -> str:
44
44
  ``QLocalServer`` names it)."""
45
45
  if sys.platform == "win32":
46
46
  return rf"\\.\pipe\{NAME}"
47
- base = os.environ.get("XDG_RUNTIME_DIR") or os.environ.get("TMPDIR") or "/tmp"
47
+ base = os.environ.get("XDG_RUNTIME_DIR") or os.environ.get("TMPDIR") or _user_temp_folder() or "/tmp"
48
48
  return str(Path(base) / f"{NAME}-{os.getuid()}")
49
49
 
50
50
 
51
+ _temp_folder: list[str] = []
52
+
53
+
54
+ def _user_temp_folder() -> str:
55
+ """The Mac's own per-user temp folder (``/var/folders/…/T/``), for a process
56
+ started without ``$TMPDIR``: Claude Desktop starts the MCP server that way,
57
+ while the app, opened from the Finder, listens there."""
58
+ if sys.platform != "darwin":
59
+ return ""
60
+ if not _temp_folder:
61
+ try:
62
+ found = subprocess.run(["/usr/bin/getconf", "DARWIN_USER_TEMP_DIR"], capture_output=True,
63
+ text=True, timeout=5).stdout.strip()
64
+ except (OSError, subprocess.SubprocessError):
65
+ found = ""
66
+ _temp_folder.append(found)
67
+ return _temp_folder[0]
68
+
69
+
51
70
  def send(message: dict, timeout: float = 5.0) -> dict:
52
71
  """Send one message to the running app and return its answer.
53
72
 
@@ -161,7 +161,20 @@ class WorkflowPage(QWidget):
161
161
  root.setSpacing(0)
162
162
  self.header = PageHeader(self.workflow.name, self._subtitle())
163
163
  #: Reproducibility, as last computed (a run finishing, a save): see refresh_title.
164
- self._repro = reproducibility.status(self.workflow, getattr(document, "record", None), None, self.folder)
164
+ #: The first check hashes the inputs on disk (a 700 MB log takes a second or two),
165
+ #: so it runs in the background; hashing does not hold Python's lock.
166
+ record = getattr(document, "record", None)
167
+ self._repro = reproducibility.Status("unchecked", "Checking the inputs…") if record is not None \
168
+ and record.results else reproducibility.status(self.workflow, record, None, self.folder)
169
+ if record is not None and record.results:
170
+ workflow, folder = self.workflow, self.folder
171
+
172
+ def first_check(status) -> None:
173
+ if self.run is None: # a run finished meanwhile has the newer word
174
+ self._repro = status
175
+ self.refresh_title()
176
+ run_in_background(lambda: reproducibility.status(workflow, record, None, folder), first_check,
177
+ lambda _message: None)
165
178
  # Canvas: the boxes and wires. Summary: every result's figure, and the process map.
166
179
  self.summary_switch = SegmentedControl(["Canvas", "Summary", "Runs"], compact=True)
167
180
  self.summary_switch.setToolTip("Canvas: the boxes and wires.\nSummary: every result's key "
@@ -579,8 +592,19 @@ class WorkflowPage(QWidget):
579
592
  def work():
580
593
  if blocked:
581
594
  # Custom boxes wait for the user's OK: run with them marked unavailable.
582
- return self._run_with_blocked(workflow, changed, previous, stop, relay, blocked)
583
- return self.runner.run(workflow, changed, previous, stop, on_status=relay.status.emit)
595
+ run = self._run_with_blocked(workflow, changed, previous, stop, relay, blocked)
596
+ else:
597
+ run = self.runner.run(workflow, changed, previous, stop, on_status=relay.status.emit)
598
+ # The reproducibility badge, the snapshot and a save fingerprint every result;
599
+ # do it here, off the window's thread, and they find the codes remembered.
600
+ from ...flow.record import fingerprint
601
+ for result in list(run.results.values()):
602
+ if result.status == DONE:
603
+ try:
604
+ fingerprint(result.value, remember=True)
605
+ except Exception: # noqa: BLE001 - a value that cannot be fingerprinted is reported later
606
+ pass
607
+ return run
584
608
 
585
609
  run_in_background(work, relay.finished.emit, lambda message: relay.finished.emit(message))
586
610