openprocess 0.13.0__tar.gz → 0.13.1__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.
- {openprocess-0.13.0/openprocess.egg-info → openprocess-0.13.1}/PKG-INFO +15 -2
- {openprocess-0.13.0 → openprocess-0.13.1}/README.md +14 -1
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/__init__.py +1 -1
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/__init__.py +5 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/input.py +5 -9
- openprocess-0.13.1/openprocess/flow/loading.py +239 -0
- openprocess-0.13.1/openprocess/flow/parallel.py +117 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/page.py +27 -3
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/app.py +93 -12
- openprocess-0.13.1/openprocess/gui/studio/connect.py +212 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/connections.py +10 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/definition_view.py +12 -4
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/documents.py +5 -1
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mcp/connecting.md +15 -6
- openprocess-0.13.1/openprocess/mcp/install.py +273 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mcp/server.py +4 -1
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/conformance/alignments.py +15 -3
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/conformance/token_replay.py +12 -4
- openprocess-0.13.1/openprocess/mining/parallel.py +81 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/petrinet.py +13 -0
- openprocess-0.13.1/openprocess/parallel.py +374 -0
- {openprocess-0.13.0 → openprocess-0.13.1/openprocess.egg-info}/PKG-INFO +15 -2
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess.egg-info/SOURCES.txt +9 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_mcp.py +37 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_mcp_gui.py +31 -0
- openprocess-0.13.1/tests/test_parallel_and_loading.py +175 -0
- openprocess-0.13.1/tests/test_parallel_boxes.py +178 -0
- openprocess-0.13.1/tests/test_researcher_api.py +156 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_studio_gui.py +1 -1
- openprocess-0.13.1/tests/test_studio_lazy.py +70 -0
- openprocess-0.13.0/openprocess/mcp/install.py +0 -127
- {openprocess-0.13.0 → openprocess-0.13.1}/LICENSE +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/cpnpy/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/analysis/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/analysis/state_space.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/analysis/state_space_process.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/citation.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/cli.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/exercises/pack.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/box.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/check.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/compare.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/cpn.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/discover.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/filter.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/measure.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/output.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/predict.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/science.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/boxes/sweeps.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/changes.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/convert.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/datasets.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/derivation.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/explain.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/figures.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/history.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/library.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/provenance.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/record.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/report.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/reproducibility.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/runner.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/script.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/snapshots.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/studio.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/sweep.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/types.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/flow/workflow.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/app.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/arc_editing.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/canvas.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/canvas.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/picker.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/runs.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/stepper.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/summary.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/templates.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/flow/viewers.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/items.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/learn/answer_boxes.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/learn/concealment.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/learn/mode.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/panning.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/resources/openprocess-icon.png +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/__main__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/alignment_view.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/charts.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/cite.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/command_palette.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/compare_page.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/cpn_page.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/derivation_view.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/dotted_chart.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/file_dialogs.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/filter_dialog.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/graph_builders.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/graph_view.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/instances.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/loading.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/log_editor.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/log_page.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/markdown_view.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/mathtext.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/ml_highlighter.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/model_page.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/motion.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/net_comparison.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/notes_overlay.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/petri_page.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/provenance.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/regions_view.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/remote.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/review.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/sidebar.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/spaces.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/style.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/tool_icons.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/updates.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/widgets.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/workers.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/studio/workspace.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/theme.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/gui/tidy.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/io/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/io/cpn_reader.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/io/cpn_writer.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/answers.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/checks.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/computed.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/context.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/exam.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/exercise-packs.md +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/importer.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/notation.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/pack.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/learn/sheet.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mcp/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mcp/__main__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mcp/api.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/analysis.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/bpmn.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/cases.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/columns.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/compare_nets.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/conformance/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/conformance/quality.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/csv_import.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/definitions.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/dfg.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/discovery/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/discovery/alpha.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/discovery/heuristics.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/discovery/inductive.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/discovery/state_regions.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/dotted.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/drawing.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/filtering.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/footprint.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/invariants.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/layout.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/log.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/ocel.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/playout.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/pm4py_bridge.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/pnml.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/processtree.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/regions.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/stats.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/structure.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/transition_system.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/mining/xes.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/ast_nodes.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/builtins.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/colorsets.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/errors.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/evaluator.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/lexer.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/multiset.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/parser.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/ml/values.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/model/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/model/declarations.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/model/examples.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/model/net.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/model/plain.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/packages.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/palette.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/references.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/sim/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/sim/binding.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/sim/export.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/sim/performance.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/sim/simulator.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/teaching/__init__.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/teaching/answers.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/teaching/checks.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/teaching/pack.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess/teaching/sheet.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess.egg-info/dependency_links.txt +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess.egg-info/entry_points.txt +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess.egg-info/requires.txt +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/openprocess.egg-info/top_level.txt +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/pyproject.toml +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/setup.cfg +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_arc_editing.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_bpmn.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_citation.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_colorsets.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_columns.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_cpn_course_model.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_definitions.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_docs.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_exercises_gui.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_flow.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_flow_gui.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_framework_contract.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_hierarchy.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_history.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_io.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_large_logs.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_learn.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_learn_gui.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_log_editing.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_measure.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_mining.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_ml.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_multiset.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_ocel.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_packages.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_performance.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_regions.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_rename.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_report.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_review_fixes.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_simulation.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_snapshots.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_state_space.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_teaching.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_tidy.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_updates.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_wf_structure.py +0 -0
- {openprocess-0.13.0 → openprocess-0.13.1}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprocess
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.1
|
|
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.
|
|
51
|
+
__version__ = "0.13.1"
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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"]
|
|
@@ -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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
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
|
|