openprocess 0.15.0__tar.gz → 0.16.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.15.0/openprocess.egg-info → openprocess-0.16.1}/PKG-INFO +18 -13
- {openprocess-0.15.0 → openprocess-0.16.1}/README.md +17 -12
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/__init__.py +1 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/cli.py +28 -14
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/pack.md +1 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/__init__.py +1 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/record.py +113 -46
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/report.py +1 -1
- openprocess-0.16.1/openprocess/flow/reproducibility.py +110 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/studio.py +2 -2
- openprocess-0.16.1/openprocess/flow/versions.py +102 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/workflow.py +2 -2
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/page.py +112 -39
- openprocess-0.16.1/openprocess/gui/flow/versions.py +188 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/learn/mode.py +8 -3
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/app.py +31 -10
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/documents.py +2 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/dotted_chart.py +41 -6
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/workspace.py +7 -2
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/checks.py +1 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/computed.py +1 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/exercise-packs.md +5 -5
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/pack.py +2 -2
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/api.py +9 -8
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/connecting.md +1 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/server.py +5 -5
- {openprocess-0.15.0 → openprocess-0.16.1/openprocess.egg-info}/PKG-INFO +18 -13
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/SOURCES.txt +2 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_dotted_chart.py +25 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_flow.py +11 -5
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_flow_gui.py +96 -28
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_framework_contract.py +39 -5
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_learn.py +1 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_learn_gui.py +1 -1
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_mcp.py +3 -3
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_parallel_boxes.py +2 -2
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_researcher_api.py +2 -2
- openprocess-0.16.1/tests/test_snapshots.py +98 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_studio_gui.py +35 -4
- openprocess-0.15.0/openprocess/flow/reproducibility.py +0 -79
- openprocess-0.15.0/openprocess/gui/flow/runs.py +0 -130
- openprocess-0.15.0/tests/test_snapshots.py +0 -55
- {openprocess-0.15.0 → openprocess-0.16.1}/LICENSE +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/cpnpy/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/analysis/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/analysis/state_space.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/analysis/state_space_process.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/citation.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/box.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/check.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/compare.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/cpn.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/discover.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/filter.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/input.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/measure.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/output.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/predict.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/science.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/sweeps.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/changes.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/convert.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/datasets.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/derivation.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/explain.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/figures.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/history.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/library.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/loading.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/parallel.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/provenance.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/runner.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/script.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/snapshots.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/sweep.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/types.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/app.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/arc_editing.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/canvas.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/canvas.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/picker.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/report.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/stepper.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/templates.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/viewers.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/items.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/learn/answer_boxes.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/learn/concealment.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/panning.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/resources/openprocess-icon.png +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/__main__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/alignment_view.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/charts.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/cite.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/command_palette.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/compare_page.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/connect.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/connections.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/cpn_page.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/definition_view.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/derivation_view.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/file_dialogs.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/filter_dialog.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/graph_builders.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/graph_view.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/instances.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/loading.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/log_editor.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/log_page.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/markdown_view.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/mathtext.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/ml_highlighter.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/model_page.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/motion.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/net_comparison.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/notes_overlay.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/petri_page.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/provenance.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/regions_view.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/remote.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/review.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/sidebar.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/spaces.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/style.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/tool_icons.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/updates.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/widgets.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/workers.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/theme.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/tidy.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/io/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/io/cpn_reader.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/io/cpn_writer.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/answers.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/context.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/exam.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/importer.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/notation.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/sheet.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/__main__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/install.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/analysis.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/bpmn.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/cases.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/columns.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/compare_nets.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/conformance/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/conformance/alignments.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/conformance/quality.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/conformance/token_replay.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/csv_import.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/definitions.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/dfg.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/alpha.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/heuristics.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/inductive.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/state_regions.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/dotted.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/drawing.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/filtering.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/footprint.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/invariants.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/layout.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/log.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/ocel.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/parallel.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/petrinet.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/playout.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/pm4py_bridge.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/pnml.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/processtree.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/regions.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/stats.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/structure.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/transition_system.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/xes.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/ast_nodes.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/builtins.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/colorsets.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/errors.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/evaluator.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/lexer.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/multiset.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/parser.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/values.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/declarations.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/examples.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/net.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/plain.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/packages.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/palette.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/parallel.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/references.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/binding.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/export.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/performance.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/simulator.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/__init__.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/answers.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/checks.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/pack.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/sheet.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/dependency_links.txt +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/entry_points.txt +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/requires.txt +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/top_level.txt +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/pyproject.toml +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/setup.cfg +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_arc_editing.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_bpmn.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_citation.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_colorsets.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_columns.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_cpn_course_model.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_definitions.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_docs.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_exercises_gui.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_hierarchy.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_history.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_io.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_large_log_page.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_large_logs.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_log_editing.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_mcp_gui.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_measure.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_mining.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_ml.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_multiset.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_ocel.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_packages.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_parallel_and_loading.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_performance.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_regions.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_rename.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_report.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_review_fixes.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_simulation.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_state_space.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_studio_lazy.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_teaching.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_tidy.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_updates.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_wf_structure.py +0 -0
- {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprocess
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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
|
|
@@ -73,7 +73,7 @@ tools (and, until version 0.7, went by the name CPNpy):
|
|
|
73
73
|
| **Petri nets & WF-nets** | Draw nets the way the lectures do. Get a soundness verdict with a counterexample for every violation, and replay it in the token game. Also: behavioural properties, P- and T-invariants, the footprint matrix, the reachability graph, PNML import and export. | WoPeD, ProM, pen and paper |
|
|
74
74
|
| **Process mining** | Import XES or CSV logs, OCEL 2.0 object-centric logs, or type textbook logs like `[<a,b,c>^3, <a,c>^2]`. Filter them, explore variants, the dotted chart and the process map. Discover models (α-algorithm, Inductive Miner, Heuristics Miner, state-based regions). Check conformance (token replay, alignments, precision…) and compare logs. | ProM, Disco |
|
|
75
75
|
| **Coloured Petri nets** | Open, edit and save CPN Tools models (`.cpn`), hierarchical ones included. Step through or simulate them, compute the state space, and export a simulation as an event log to mine. | CPN Tools / CPN IDE |
|
|
76
|
-
| **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.
|
|
76
|
+
| **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.analysis` file with everything needed to get the same numbers back. Your own algorithm is one Python function in a `boxes/` folder. | RapidProM |
|
|
77
77
|
| **Learn** | A mode of its own for worksheets, built on the app: answer in boxes on the sheet (sets, markings, matrices, cuts and trees, alignments, a net in the editor or a workflow on the canvas beside it) and press **Check**. Most answers are checked automatically, often against answers worked out from the given log or net. Packs are plain Markdown, can be exams with a clock and points, and a past exam imports as a skeleton pack. Demo exercises included. | Answer sheets, a notes app and guesswork |
|
|
78
78
|
| **Folders** | Open a folder such as *Week 2*: every log and net in it is listed in the sidebar, with its subfolders. The folder and the app stay in step both ways: new nets and edits are saved into it as you go, and changes made in Finder show up by themselves. | Finder windows and *File ▸ Open* every time |
|
|
79
79
|
|
|
@@ -598,7 +598,7 @@ without distractions, and *Exit* brings your folder back exactly as it was.
|
|
|
598
598
|
writes the marks as CSV. A pack can also give every student a **variant**
|
|
599
599
|
of its own (a log played out from a net with a seed made from their name).
|
|
600
600
|
- **Your work is saved as you go**, in the exercise's folder:
|
|
601
|
-
`my answers.json`, `my answer.pnml` for a net, `my workflow.
|
|
601
|
+
`my answers.json`, `my answer.pnml` for a net, `my workflow.analysis` for a
|
|
602
602
|
workflow and `my notes.md`.
|
|
603
603
|
|
|
604
604
|
**Writing a pack** (for a course or an exam) is plain Markdown: put an
|
|
@@ -796,9 +796,9 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
796
796
|
|  |  |
|
|
797
797
|
|
|
798
798
|
- **An analysis opens on its Report.** Its header has **Report | Canvas |
|
|
799
|
-
|
|
800
|
-
Export experiment and Save. The sidebar beside it
|
|
801
|
-
analyses and, folded under them, its logs.
|
|
799
|
+
Versions**, then **+ Add box**, **Run ▶**, and **⋯** for Run from scratch,
|
|
800
|
+
Keep as version, Record, Export experiment and Save. The sidebar beside it
|
|
801
|
+
lists the folder's analyses and, folded under them, its logs.
|
|
802
802
|
- **Report** is every box's result, in the order the boxes run, on one
|
|
803
803
|
scrolling page: a section per box with the group it is in, its name, the
|
|
804
804
|
line the canvas shows under it, its status, the result as the side panel
|
|
@@ -846,9 +846,14 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
846
846
|
- **Only what changed runs again.** Every box shows a status dot: waiting,
|
|
847
847
|
running, done, failed (the error is on the Result tab; the rest keeps
|
|
848
848
|
working), or waiting for your OK.
|
|
849
|
-
- **
|
|
850
|
-
|
|
851
|
-
|
|
849
|
+
- **Versions** is where the numbers stand against the record and every
|
|
850
|
+
version kept. The first run with no failure is kept as v1; from then on the
|
|
851
|
+
badge beside the title says *v1 · reproducible*, *v1 · 2 changes* or
|
|
852
|
+
*Changed since v1*, and **Keep as version…** records the run as v2 with a
|
|
853
|
+
note. Any version can be compared with now (the boxes whose result differs,
|
|
854
|
+
first) or restored whole. **Record** shows the workflow as Python and the
|
|
855
|
+
file; **Run from scratch** says what changed since the version was kept
|
|
856
|
+
before computing everything again.
|
|
852
857
|
- **Your own boxes**: a `.py` file in the folder's `boxes/` subfolder (see
|
|
853
858
|
below). The app asks once per folder before running them, and reloads a
|
|
854
859
|
box when its file is saved.
|
|
@@ -910,7 +915,7 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
910
915
|
|
|
911
916
|
Every algorithm is also a **box**: a Python function with type hints that
|
|
912
917
|
the app (from 0.7) draws on a canvas. A workflow of boxes runs from Python
|
|
913
|
-
or from the command line, and its `.
|
|
918
|
+
or from the command line, and its `.analysis` file records what is needed
|
|
914
919
|
to get the same numbers again: fingerprints of the input files and of the
|
|
915
920
|
box code, every seed, and the installed packages.
|
|
916
921
|
|
|
@@ -928,12 +933,12 @@ wf.connect(log, model); wf.connect(model, fit, "model"); wf.connect(log, fit, "l
|
|
|
928
933
|
run = Runner().run(wf)
|
|
929
934
|
print(run.value(fit).metrics) # fitness, precision, generalisation, simplicity
|
|
930
935
|
print(run.result(model).explanation.steps) # how the Inductive Miner got there
|
|
931
|
-
save(wf, "orders.
|
|
936
|
+
save(wf, "orders.analysis", run)
|
|
932
937
|
```
|
|
933
938
|
|
|
934
939
|
```bash
|
|
935
|
-
openprocess run orders.
|
|
936
|
-
openprocess run orders.
|
|
940
|
+
openprocess run orders.analysis --check # re-run; fails if any result differs from the record
|
|
941
|
+
openprocess run orders.analysis --sweep noise=0..0.5 step 0.1
|
|
937
942
|
openprocess boxes # every box (listed in docs/boxes.md), with a folder's boxes/
|
|
938
943
|
openprocess datasets list # the BPI Challenge, Sepsis, ... logs by name
|
|
939
944
|
```
|
|
@@ -31,7 +31,7 @@ tools (and, until version 0.7, went by the name CPNpy):
|
|
|
31
31
|
| **Petri nets & WF-nets** | Draw nets the way the lectures do. Get a soundness verdict with a counterexample for every violation, and replay it in the token game. Also: behavioural properties, P- and T-invariants, the footprint matrix, the reachability graph, PNML import and export. | WoPeD, ProM, pen and paper |
|
|
32
32
|
| **Process mining** | Import XES or CSV logs, OCEL 2.0 object-centric logs, or type textbook logs like `[<a,b,c>^3, <a,c>^2]`. Filter them, explore variants, the dotted chart and the process map. Discover models (α-algorithm, Inductive Miner, Heuristics Miner, state-based regions). Check conformance (token replay, alignments, precision…) and compare logs. | ProM, Disco |
|
|
33
33
|
| **Coloured Petri nets** | Open, edit and save CPN Tools models (`.cpn`), hierarchical ones included. Step through or simulate them, compute the state space, and export a simulation as an event log to mine. | CPN Tools / CPN IDE |
|
|
34
|
-
| **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.
|
|
34
|
+
| **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.analysis` file with everything needed to get the same numbers back. Your own algorithm is one Python function in a `boxes/` folder. | RapidProM |
|
|
35
35
|
| **Learn** | A mode of its own for worksheets, built on the app: answer in boxes on the sheet (sets, markings, matrices, cuts and trees, alignments, a net in the editor or a workflow on the canvas beside it) and press **Check**. Most answers are checked automatically, often against answers worked out from the given log or net. Packs are plain Markdown, can be exams with a clock and points, and a past exam imports as a skeleton pack. Demo exercises included. | Answer sheets, a notes app and guesswork |
|
|
36
36
|
| **Folders** | Open a folder such as *Week 2*: every log and net in it is listed in the sidebar, with its subfolders. The folder and the app stay in step both ways: new nets and edits are saved into it as you go, and changes made in Finder show up by themselves. | Finder windows and *File ▸ Open* every time |
|
|
37
37
|
|
|
@@ -556,7 +556,7 @@ without distractions, and *Exit* brings your folder back exactly as it was.
|
|
|
556
556
|
writes the marks as CSV. A pack can also give every student a **variant**
|
|
557
557
|
of its own (a log played out from a net with a seed made from their name).
|
|
558
558
|
- **Your work is saved as you go**, in the exercise's folder:
|
|
559
|
-
`my answers.json`, `my answer.pnml` for a net, `my workflow.
|
|
559
|
+
`my answers.json`, `my answer.pnml` for a net, `my workflow.analysis` for a
|
|
560
560
|
workflow and `my notes.md`.
|
|
561
561
|
|
|
562
562
|
**Writing a pack** (for a course or an exam) is plain Markdown: put an
|
|
@@ -754,9 +754,9 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
754
754
|
|  |  |
|
|
755
755
|
|
|
756
756
|
- **An analysis opens on its Report.** Its header has **Report | Canvas |
|
|
757
|
-
|
|
758
|
-
Export experiment and Save. The sidebar beside it
|
|
759
|
-
analyses and, folded under them, its logs.
|
|
757
|
+
Versions**, then **+ Add box**, **Run ▶**, and **⋯** for Run from scratch,
|
|
758
|
+
Keep as version, Record, Export experiment and Save. The sidebar beside it
|
|
759
|
+
lists the folder's analyses and, folded under them, its logs.
|
|
760
760
|
- **Report** is every box's result, in the order the boxes run, on one
|
|
761
761
|
scrolling page: a section per box with the group it is in, its name, the
|
|
762
762
|
line the canvas shows under it, its status, the result as the side panel
|
|
@@ -804,9 +804,14 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
804
804
|
- **Only what changed runs again.** Every box shows a status dot: waiting,
|
|
805
805
|
running, done, failed (the error is on the Result tab; the rest keeps
|
|
806
806
|
working), or waiting for your OK.
|
|
807
|
-
- **
|
|
808
|
-
|
|
809
|
-
|
|
807
|
+
- **Versions** is where the numbers stand against the record and every
|
|
808
|
+
version kept. The first run with no failure is kept as v1; from then on the
|
|
809
|
+
badge beside the title says *v1 · reproducible*, *v1 · 2 changes* or
|
|
810
|
+
*Changed since v1*, and **Keep as version…** records the run as v2 with a
|
|
811
|
+
note. Any version can be compared with now (the boxes whose result differs,
|
|
812
|
+
first) or restored whole. **Record** shows the workflow as Python and the
|
|
813
|
+
file; **Run from scratch** says what changed since the version was kept
|
|
814
|
+
before computing everything again.
|
|
810
815
|
- **Your own boxes**: a `.py` file in the folder's `boxes/` subfolder (see
|
|
811
816
|
below). The app asks once per folder before running them, and reloads a
|
|
812
817
|
box when its file is saved.
|
|
@@ -868,7 +873,7 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
868
873
|
|
|
869
874
|
Every algorithm is also a **box**: a Python function with type hints that
|
|
870
875
|
the app (from 0.7) draws on a canvas. A workflow of boxes runs from Python
|
|
871
|
-
or from the command line, and its `.
|
|
876
|
+
or from the command line, and its `.analysis` file records what is needed
|
|
872
877
|
to get the same numbers again: fingerprints of the input files and of the
|
|
873
878
|
box code, every seed, and the installed packages.
|
|
874
879
|
|
|
@@ -886,12 +891,12 @@ wf.connect(log, model); wf.connect(model, fit, "model"); wf.connect(log, fit, "l
|
|
|
886
891
|
run = Runner().run(wf)
|
|
887
892
|
print(run.value(fit).metrics) # fitness, precision, generalisation, simplicity
|
|
888
893
|
print(run.result(model).explanation.steps) # how the Inductive Miner got there
|
|
889
|
-
save(wf, "orders.
|
|
894
|
+
save(wf, "orders.analysis", run)
|
|
890
895
|
```
|
|
891
896
|
|
|
892
897
|
```bash
|
|
893
|
-
openprocess run orders.
|
|
894
|
-
openprocess run orders.
|
|
898
|
+
openprocess run orders.analysis --check # re-run; fails if any result differs from the record
|
|
899
|
+
openprocess run orders.analysis --sweep noise=0..0.5 step 0.1
|
|
895
900
|
openprocess boxes # every box (listed in docs/boxes.md), with a folder's boxes/
|
|
896
901
|
openprocess datasets list # the BPI Challenge, Sepsis, ... logs by name
|
|
897
902
|
```
|
|
@@ -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.
|
|
51
|
+
__version__ = "0.16.1"
|
|
52
52
|
|
|
53
53
|
__all__ = [
|
|
54
54
|
"Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
|
|
@@ -19,7 +19,7 @@ Commands
|
|
|
19
19
|
``stats``, ``filter``, ``discover``, ``conform``,
|
|
20
20
|
``soundness``, ``invariants``
|
|
21
21
|
``exercises`` ``check``, ``marks``, ``import``, ``computes``: exercise packs (OpenProcess Learn)
|
|
22
|
-
``run`` run a ``.
|
|
22
|
+
``run`` run a ``.analysis`` workflow headless; ``--check`` re-runs and
|
|
23
23
|
compares with the record, ``--sweep`` varies a setting,
|
|
24
24
|
``--lock`` writes the requirements the record names,
|
|
25
25
|
``--export`` and ``--report`` write a zip and an HTML page
|
|
@@ -447,12 +447,19 @@ def command_exercises_computes(arguments: argparse.Namespace) -> int:
|
|
|
447
447
|
# ---------------------------------------------------------------------------
|
|
448
448
|
def command_run(arguments: argparse.Namespace) -> int:
|
|
449
449
|
"""Run a workflow file without the app, and say what every box gave."""
|
|
450
|
-
from .flow import Runner, Sweep, check, differences, library_for,
|
|
451
|
-
from .flow.record import requirements_lock
|
|
450
|
+
from .flow import Runner, Sweep, check, differences, library_for, save
|
|
451
|
+
from .flow.record import load_all, requirements_lock
|
|
452
452
|
path = Path(arguments.workflow)
|
|
453
453
|
folder = path.parent
|
|
454
454
|
library = library_for(folder)
|
|
455
|
-
workflow, record =
|
|
455
|
+
workflow, record, versions = load_all(path, library)
|
|
456
|
+
if arguments.against is not None:
|
|
457
|
+
version = versions.get(arguments.against)
|
|
458
|
+
if version is None:
|
|
459
|
+
print(f"openprocess: {path.name} has no version {arguments.against} "
|
|
460
|
+
f"(it has {', '.join(f'v{v.number}' for v in versions.items) or 'none'})", file=sys.stderr)
|
|
461
|
+
return 2
|
|
462
|
+
record = version.record
|
|
456
463
|
if arguments.lock:
|
|
457
464
|
target = folder / "requirements.lock"
|
|
458
465
|
target.write_text(requirements_lock(record), encoding="utf-8")
|
|
@@ -504,6 +511,8 @@ def command_run(arguments: argparse.Namespace) -> int:
|
|
|
504
511
|
if arguments.report:
|
|
505
512
|
from .flow.report import write_report
|
|
506
513
|
print(f"Wrote {write_report(arguments.report, workflow=workflow, run=run, folder=folder)}")
|
|
514
|
+
against = f"v{arguments.against}" if arguments.against is not None else (
|
|
515
|
+
f"v{versions.latest.number}" if versions.latest else "the record")
|
|
507
516
|
if arguments.check:
|
|
508
517
|
if not record.results:
|
|
509
518
|
# Nothing to compare is not a pass: a CI job would claim a reproduction that never happened.
|
|
@@ -513,15 +522,18 @@ def command_run(arguments: argparse.Namespace) -> int:
|
|
|
513
522
|
report = check(workflow, record, run, complete=True)
|
|
514
523
|
if report:
|
|
515
524
|
for line in report:
|
|
516
|
-
print(f"DIFFERS: {line}")
|
|
525
|
+
print(f"DIFFERS from {against}: {line}")
|
|
517
526
|
return 1
|
|
518
|
-
print(f"Every recorded
|
|
527
|
+
print(f"Every result recorded in {against} was reproduced ({len(record.results)} boxes).")
|
|
519
528
|
elif arguments.save:
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
print(f"A box failed, so {path.name} keeps
|
|
529
|
+
if run.failed():
|
|
530
|
+
save(workflow, path, run, folder, previous=record, versions=versions)
|
|
531
|
+
print(f"A box failed, so {path.name} keeps " + (f"{against} as its record" if record.results
|
|
532
|
+
else "no record yet"))
|
|
523
533
|
else:
|
|
524
|
-
|
|
534
|
+
save(workflow, path, run, folder, previous=record, versions=versions, keep=arguments.note or "")
|
|
535
|
+
print(f"Kept the results as v{versions.latest.number} in {path.name}"
|
|
536
|
+
+ (f": {arguments.note}" if arguments.note else ""))
|
|
525
537
|
return 0 if run.ok else 1
|
|
526
538
|
|
|
527
539
|
|
|
@@ -713,11 +725,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
713
725
|
"computes", help="list every compute: an answer block may use")
|
|
714
726
|
exercise_computes.set_defaults(handler=command_exercises_computes)
|
|
715
727
|
|
|
716
|
-
run = subparsers.add_parser("run", help="run a .
|
|
717
|
-
run.add_argument("workflow", help="the .
|
|
728
|
+
run = subparsers.add_parser("run", help="run a .analysis workflow without the app")
|
|
729
|
+
run.add_argument("workflow", help="the .analysis file")
|
|
718
730
|
run.add_argument("--check", action="store_true",
|
|
719
|
-
help="re-run and fail if any result differs from the recorded one")
|
|
720
|
-
run.add_argument("--
|
|
731
|
+
help="re-run and fail if any result differs from the recorded one (the latest version)")
|
|
732
|
+
run.add_argument("--against", type=int, metavar="N", help="check against version N instead of the latest")
|
|
733
|
+
run.add_argument("--save", action="store_true", help="keep the results in the file as the next version")
|
|
734
|
+
run.add_argument("--note", metavar="TEXT", help="with --save: the version's note (what changed, or why)")
|
|
721
735
|
run.add_argument("--sweep", action="append", metavar="SETTING=VALUES",
|
|
722
736
|
help="vary a setting, e.g. noise=0..0.5 step 0.1 or n3.seed=1,2,3")
|
|
723
737
|
run.add_argument("--lock", action="store_true",
|
|
@@ -7,7 +7,7 @@ straight away, and the app never shows you a result you are meant to work out
|
|
|
7
7
|
until you ask for it.
|
|
8
8
|
|
|
9
9
|
Your answers are saved in each exercise's folder (*my answers.json*, and
|
|
10
|
-
*my answer.pnml* for a net you draw, *my workflow.
|
|
10
|
+
*my answer.pnml* for a net you draw, *my workflow.analysis* for a workflow
|
|
11
11
|
you build), so you can stop and carry on later.
|
|
12
12
|
|
|
13
13
|
To write your own pack, copy this folder and change the `question.md` files:
|
|
@@ -24,7 +24,7 @@ Python::
|
|
|
24
24
|
wf.connect(log, model); wf.connect(model, fit, "model"); wf.connect(log, fit, "log")
|
|
25
25
|
run = Runner().run(wf)
|
|
26
26
|
print(run.value(fit).metrics)
|
|
27
|
-
save(wf, "demo.
|
|
27
|
+
save(wf, "demo.analysis", run) # with the record that makes it reproducible
|
|
28
28
|
|
|
29
29
|
Work over many independent items (variants, cases, resamples) runs on all
|
|
30
30
|
cores with :func:`parallel_map`, with exactly the result of a loop.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
"""The ``.
|
|
1
|
+
"""The ``.analysis`` file: a workflow, and the record that makes it reproducible.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
connections, the groups,
|
|
3
|
+
An analysis file is JSON: the boxes, their settings and positions, the
|
|
4
|
+
connections, the groups, its **versions** (:mod:`.versions`: the record as
|
|
5
|
+
it was each time the user kept it) and the **record** of the latest version:
|
|
5
6
|
|
|
6
7
|
* the versions of OpenProcess, Python and the optional libraries;
|
|
7
8
|
* the environment: every installed package and its version (the lock);
|
|
@@ -34,7 +35,44 @@ from .runner import DONE, Run
|
|
|
34
35
|
from .types import Dataset, Figure, Predictions, Scores, Table, Text
|
|
35
36
|
from .workflow import Workflow
|
|
36
37
|
|
|
37
|
-
FORMAT = "
|
|
38
|
+
FORMAT = "analysis/1"
|
|
39
|
+
|
|
40
|
+
#: The extension of an analysis file, and the ones it had before (``.cpnflow``,
|
|
41
|
+
#: from when the app was CPNpy: the content is the same, only the name changed).
|
|
42
|
+
EXTENSION = ".analysis"
|
|
43
|
+
OLD_EXTENSIONS = (".cpnflow",)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_analysis_file(path) -> bool:
|
|
47
|
+
"""Whether ``path`` is an analysis file by its name, under either extension."""
|
|
48
|
+
return str(path).lower().endswith((EXTENSION,) + OLD_EXTENSIONS)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def analysis_name(path) -> Path:
|
|
52
|
+
"""``path`` under the current extension (``orders.cpnflow`` → ``orders.analysis``)."""
|
|
53
|
+
path = Path(path)
|
|
54
|
+
return path.with_name(path.stem + EXTENSION) if path.suffix.lower() in OLD_EXTENSIONS else path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def carry_over(folder, depth: int = 3) -> list[Path]:
|
|
58
|
+
"""Rename every ``.cpnflow`` under ``folder`` to ``.analysis`` (the content is
|
|
59
|
+
read either way); a file whose new name is taken is left. Returns the new paths."""
|
|
60
|
+
renamed = []
|
|
61
|
+
root = Path(folder)
|
|
62
|
+
for old in OLD_EXTENSIONS:
|
|
63
|
+
for source in sorted(root.rglob(f"*{old}")):
|
|
64
|
+
if len(source.relative_to(root).parts) - 1 > depth or any(
|
|
65
|
+
part.startswith(".") for part in source.relative_to(root).parts):
|
|
66
|
+
continue
|
|
67
|
+
target = analysis_name(source)
|
|
68
|
+
if target.exists():
|
|
69
|
+
continue
|
|
70
|
+
try:
|
|
71
|
+
source.replace(target)
|
|
72
|
+
except OSError:
|
|
73
|
+
continue
|
|
74
|
+
renamed.append(target)
|
|
75
|
+
return renamed
|
|
38
76
|
|
|
39
77
|
|
|
40
78
|
# ---------------------------------------------------------------------------
|
|
@@ -460,17 +498,45 @@ def differences(record: Record, workflow: Workflow, folder: str | Path | None =
|
|
|
460
498
|
# ---------------------------------------------------------------------------
|
|
461
499
|
def save(workflow: Workflow, path: str | Path, run: Run | None = None,
|
|
462
500
|
folder: str | Path | None = None, lock: bool = True, history: list[dict] | None = None,
|
|
463
|
-
previous: Record | None = None
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
501
|
+
previous: Record | None = None, versions: "Versions | None" = None,
|
|
502
|
+
keep: str | None = None) -> Record:
|
|
503
|
+
"""Write the workflow, its versions and its record to ``path``; returns the record.
|
|
504
|
+
|
|
505
|
+
``history``: the analysis's runs (see :mod:`.snapshots`), kept in the file.
|
|
506
|
+
``previous``: the file's record so far, and ``versions`` the versions it
|
|
507
|
+
holds; see :func:`carried`. ``keep``: a note, to keep this run as a new
|
|
508
|
+
version (``""`` for a version without one): the record becomes this run's.
|
|
509
|
+
Without ``keep`` the record stays as it was, except that the first run in
|
|
510
|
+
which no box fails is kept as version 1 by itself, so a new analysis is
|
|
511
|
+
recorded without a word.
|
|
512
|
+
|
|
513
|
+
``versions`` is updated in place when a version is kept."""
|
|
514
|
+
from .versions import Versions
|
|
467
515
|
path = Path(path)
|
|
468
516
|
folder = folder if folder is not None else path.parent
|
|
469
|
-
|
|
470
|
-
if
|
|
471
|
-
|
|
517
|
+
versions = versions if versions is not None else Versions()
|
|
518
|
+
if keep is not None and (run is None or run.failed()):
|
|
519
|
+
raise ValueError("A version keeps a run in which no box failed")
|
|
520
|
+
if keep is None and previous is not None and previous.results:
|
|
521
|
+
record = carried(previous, workflow)
|
|
522
|
+
elif keep is None and (run is None or run.failed()):
|
|
523
|
+
record = make_record(workflow, None, folder, lock) # nothing to keep yet
|
|
524
|
+
else:
|
|
525
|
+
record = make_record(workflow, run, folder, lock)
|
|
526
|
+
if previous is not None and previous.scheme == record.scheme:
|
|
527
|
+
# A box that could not run here (a library it needs is missing)
|
|
528
|
+
# keeps the fingerprint recorded where it could.
|
|
529
|
+
still = {k: v for k, v in previous.results.items() if k in workflow.nodes}
|
|
530
|
+
record.results = {**still, **record.results}
|
|
531
|
+
for entry in record.inputs:
|
|
532
|
+
if entry["sha256"] == "missing" and previous is not None:
|
|
533
|
+
then = next((i for i in previous.inputs if i["node"] == entry["node"]
|
|
534
|
+
and i["setting"] == entry["setting"] and i["file"] == entry["file"]), None)
|
|
535
|
+
if then is not None:
|
|
536
|
+
entry["sha256"] = then["sha256"]
|
|
537
|
+
versions.keep(workflow, run, record, keep or ("First run" if not versions.items else ""))
|
|
472
538
|
record.history = list(history or [])
|
|
473
|
-
data = {"format": FORMAT, **workflow.to_dict(), "record": record.to_dict()}
|
|
539
|
+
data = {"format": FORMAT, **workflow.to_dict(), "versions": versions.to_list(), "record": record.to_dict()}
|
|
474
540
|
text = json.dumps(data, indent=2, ensure_ascii=False, default=str)
|
|
475
541
|
temporary = path.with_name(path.name + ".tmp")
|
|
476
542
|
temporary.write_text(text, encoding="utf-8")
|
|
@@ -478,37 +544,34 @@ def save(workflow: Workflow, path: str | Path, run: Run | None = None,
|
|
|
478
544
|
return record
|
|
479
545
|
|
|
480
546
|
|
|
481
|
-
def carried(
|
|
482
|
-
"""The record to write
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
No run, or a run in which a box failed (the input file was not found, say),
|
|
486
|
-
leaves the previous record as it was, results of boxes since removed aside.
|
|
487
|
-
A run in which no box failed replaces it; a box that could not run here (a
|
|
488
|
-
library it needs is missing) keeps its recorded fingerprint if the scheme is
|
|
489
|
-
the same, and an input file that is missing now keeps the fingerprint
|
|
490
|
-
recorded when it was read."""
|
|
547
|
+
def carried(previous: Record, workflow: Workflow) -> Record:
|
|
548
|
+
"""The record to write when no version is kept: the previous one as it
|
|
549
|
+
was, results of boxes since removed aside. Saving as you work never
|
|
550
|
+
replaces the record; keeping a version does (:func:`save` with ``keep``)."""
|
|
491
551
|
still = {k: v for k, v in previous.results.items() if k in workflow.nodes}
|
|
492
|
-
|
|
493
|
-
return Record(**{**previous.__dict__, "results": still})
|
|
494
|
-
if previous.scheme == record.scheme:
|
|
495
|
-
record.results = {**still, **record.results}
|
|
496
|
-
for entry in record.inputs:
|
|
497
|
-
if entry["sha256"] == "missing":
|
|
498
|
-
then = next((i for i in previous.inputs if i["node"] == entry["node"]
|
|
499
|
-
and i["setting"] == entry["setting"] and i["file"] == entry["file"]), None)
|
|
500
|
-
if then is not None:
|
|
501
|
-
entry["sha256"] = then["sha256"]
|
|
502
|
-
return record
|
|
552
|
+
return Record(**{**previous.__dict__, "results": still})
|
|
503
553
|
|
|
504
554
|
|
|
505
555
|
def load(path: str | Path, library=None) -> tuple[Workflow, Record]:
|
|
506
|
-
"""Read
|
|
556
|
+
"""Read an analysis file (``.analysis``, or ``.cpnflow`` as it was called)."""
|
|
557
|
+
workflow, record, _versions = load_all(path, library)
|
|
558
|
+
return workflow, record
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def load_all(path: str | Path, library=None) -> "tuple[Workflow, Record, Versions]":
|
|
562
|
+
"""Read an analysis file: its workflow, the record of its latest version, and
|
|
563
|
+
every version kept. A file from before versions (a record, no list) gives
|
|
564
|
+
its record as version 1."""
|
|
565
|
+
from .versions import Versions
|
|
507
566
|
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
508
|
-
if data.get("format", FORMAT).split("/")[0]
|
|
509
|
-
raise ValueError(f"{path} is not
|
|
567
|
+
if data.get("format", FORMAT).split("/")[0] not in ("analysis", "cpnflow"):
|
|
568
|
+
raise ValueError(f"{path} is not an analysis file")
|
|
510
569
|
workflow = Workflow.from_dict(data, library)
|
|
511
|
-
|
|
570
|
+
record = Record.from_dict(data.get("record"))
|
|
571
|
+
versions = Versions.from_list(data.get("versions"))
|
|
572
|
+
if not versions.items and record.results:
|
|
573
|
+
versions = Versions.first_from(workflow, record)
|
|
574
|
+
return workflow, record, versions
|
|
512
575
|
|
|
513
576
|
|
|
514
577
|
def check(workflow: Workflow, record: Record, run: Run, complete: bool = False) -> list[str]:
|
|
@@ -578,7 +641,7 @@ def export_experiment(workflow: Workflow, run: Run | None, target: str | Path,
|
|
|
578
641
|
name = workflow.name or "workflow"
|
|
579
642
|
lines = [f"# {name}", "", f"Exported {record.saved} with OpenProcess {record.versions.get('openprocess', '')}, "
|
|
580
643
|
f"Python {record.versions.get('python', '')}.", "",
|
|
581
|
-
"Run it again with `openprocess run " + f"{name}
|
|
644
|
+
"Run it again with `openprocess run " + f"{name}{EXTENSION} --check`: it fails if any result differs "
|
|
582
645
|
"from the recorded one. `requirements.lock` is the environment it ran in. `run.py` is the same "
|
|
583
646
|
"analysis as a plain Python script (`python run.py`). Or drop this zip on OpenProcess: it unpacks "
|
|
584
647
|
"it into a folder and runs it.", "", "## Boxes", ""]
|
|
@@ -606,7 +669,7 @@ def export_experiment(workflow: Workflow, run: Run | None, target: str | Path,
|
|
|
606
669
|
lines += ["", "## How to cite", "", citation.app_text(), "",
|
|
607
670
|
"`CITATIONS.bib` has this entry and the papers the algorithms follow, as BibTeX."]
|
|
608
671
|
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipped:
|
|
609
|
-
zipped.writestr(f"{name}
|
|
672
|
+
zipped.writestr(f"{name}{EXTENSION}", json.dumps(data, indent=2, ensure_ascii=False, default=str))
|
|
610
673
|
zipped.writestr("requirements.lock", requirements_lock(record))
|
|
611
674
|
from .script import script_text
|
|
612
675
|
zipped.writestr("run.py", script_text(packed_workflow, "run.py"))
|
|
@@ -642,11 +705,11 @@ def _describe(value) -> str:
|
|
|
642
705
|
|
|
643
706
|
|
|
644
707
|
def is_experiment(path) -> bool:
|
|
645
|
-
"""Whether ``path`` is a zip :func:`export_experiment` wrote: one
|
|
708
|
+
"""Whether ``path`` is a zip :func:`export_experiment` wrote: one analysis file at the top."""
|
|
646
709
|
import zipfile
|
|
647
710
|
try:
|
|
648
711
|
with zipfile.ZipFile(path) as zipped:
|
|
649
|
-
return sum(1 for n in zipped.namelist() if n
|
|
712
|
+
return sum(1 for n in zipped.namelist() if is_analysis_file(n) and "/" not in n) == 1
|
|
650
713
|
except (OSError, zipfile.BadZipFile):
|
|
651
714
|
return False
|
|
652
715
|
|
|
@@ -661,9 +724,9 @@ def import_experiment(archive, into) -> Path:
|
|
|
661
724
|
archive = Path(archive)
|
|
662
725
|
with zipfile.ZipFile(archive) as zipped:
|
|
663
726
|
names = zipped.namelist()
|
|
664
|
-
flows = [n for n in names if n
|
|
727
|
+
flows = [n for n in names if is_analysis_file(n) and "/" not in n]
|
|
665
728
|
if len(flows) != 1:
|
|
666
|
-
raise ValueError(f"{archive.name} is not an exported experiment (one
|
|
729
|
+
raise ValueError(f"{archive.name} is not an exported experiment (one {EXTENSION} file at the top is needed)")
|
|
667
730
|
folder = Path(into) / archive.stem
|
|
668
731
|
counter = 2
|
|
669
732
|
while folder.exists():
|
|
@@ -679,6 +742,9 @@ def import_experiment(archive, into) -> Path:
|
|
|
679
742
|
target.write_bytes(zipped.read(name))
|
|
680
743
|
flow_file = folder / flows[0]
|
|
681
744
|
data = json.loads(flow_file.read_text(encoding="utf-8"))
|
|
745
|
+
if flow_file.suffix.lower() in OLD_EXTENSIONS: # an experiment exported before 0.16
|
|
746
|
+
flow_file.unlink()
|
|
747
|
+
flow_file = analysis_name(flow_file)
|
|
682
748
|
moved: dict[tuple[str, str], str] = {}
|
|
683
749
|
for item in data.get("record", {}).get("inputs", []):
|
|
684
750
|
new = f"inputs/{Path(item['file']).name}"
|
|
@@ -728,6 +794,7 @@ def _as_file(value, stem: str):
|
|
|
728
794
|
#: (name, bytes or text) for a result worth a file of its own; the script an experiment carries uses it.
|
|
729
795
|
as_file = _as_file
|
|
730
796
|
|
|
731
|
-
__all__ = ["FORMAT", "
|
|
732
|
-
"
|
|
733
|
-
"
|
|
797
|
+
__all__ = ["EXTENSION", "FORMAT", "OLD_EXTENSIONS", "Record", "analysis_name", "as_file", "carry_over", "check",
|
|
798
|
+
"differences", "environment", "export_experiment", "fingerprint", "import_experiment",
|
|
799
|
+
"is_analysis_file", "is_experiment", "load", "load_all", "make_record", "requirements_lock", "save",
|
|
800
|
+
"sha256_file", "sha256_text", "versions"]
|
|
@@ -9,7 +9,7 @@ external assets, readable in any browser and printable.
|
|
|
9
9
|
|
|
10
10
|
The page is built from the workflow and its run alone; drawings (SVG) are
|
|
11
11
|
passed in by the app, which has the canvas to render them. So the same
|
|
12
|
-
report can be made without the app (``openprocess run analysis
|
|
12
|
+
report can be made without the app (``openprocess run orders.analysis
|
|
13
13
|
--report report.html``), minus the pictures.
|
|
14
14
|
"""
|
|
15
15
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Is this analysis reproducible, right now? One status for the header.
|
|
2
|
+
|
|
3
|
+
The record (:mod:`openprocess.flow.record`) is a file; this turns it into a
|
|
4
|
+
visible promise. :func:`status` compares the record (the latest version,
|
|
5
|
+
:mod:`.versions`) with the world now (the inputs' fingerprints, the box
|
|
6
|
+
files, the seeds, the settings, the software versions) and with the latest
|
|
7
|
+
run (every box's result against its recorded fingerprint), and says one of:
|
|
8
|
+
|
|
9
|
+
* **Not recorded yet** -- the analysis has never run without a failure;
|
|
10
|
+
* **v3 · this run reproduces it** -- every result matches the record and
|
|
11
|
+
nothing it depends on has changed;
|
|
12
|
+
* **v3 · n changes since** -- a setting, an input file, a box file, a seed
|
|
13
|
+
or a version differs from the record (a re-run may give other numbers);
|
|
14
|
+
* **Changed since v3** -- those changes, and the results differ: the
|
|
15
|
+
numbers moved for a reason you can see; keep a version when you are happy
|
|
16
|
+
with them;
|
|
17
|
+
* **Differs from v3** -- the same inputs, settings and versions gave another
|
|
18
|
+
result: a reproducibility problem.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
|
|
25
|
+
from .record import Record, check, differences
|
|
26
|
+
from .runner import BLOCKED, DONE, Run
|
|
27
|
+
from .snapshots import setting_changes, take
|
|
28
|
+
from .versions import Versions
|
|
29
|
+
from .workflow import Workflow
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Status:
|
|
34
|
+
#: "unrecorded", "reproduces", "unchecked", "changed", "moved" or "differs".
|
|
35
|
+
state: str
|
|
36
|
+
headline: str
|
|
37
|
+
details: list[str] = field(default_factory=list)
|
|
38
|
+
#: The version the record is ("v3"; "" when nothing is recorded).
|
|
39
|
+
version: str = ""
|
|
40
|
+
#: How many of the details are changes since the record (the rest: what could not be compared).
|
|
41
|
+
changes: int = 0
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def tone(self) -> str:
|
|
45
|
+
return {"reproduces": "good", "changed": "warning", "moved": "warning",
|
|
46
|
+
"differs": "critical"}.get(self.state, "muted")
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def short(self) -> str:
|
|
50
|
+
"""Two or three words for a badge; :attr:`headline` is the sentence."""
|
|
51
|
+
v = self.version
|
|
52
|
+
if self.state == "changed":
|
|
53
|
+
return f"{v} · {self.changes} change{'s' if self.changes != 1 else ''}"
|
|
54
|
+
return {"unrecorded": "Not recorded", "reproduces": f"{v} · reproducible", "differs": f"Differs from {v}",
|
|
55
|
+
"moved": f"Changed since {v}", "unchecked": f"{v} · not run"}.get(self.state, self.headline)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def can_keep(self) -> bool:
|
|
59
|
+
"""Whether keeping a version now would say something new."""
|
|
60
|
+
return self.state in ("changed", "moved", "differs")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def status(workflow: Workflow, record: Record | None, run: Run | None, folder=None,
|
|
64
|
+
versions: Versions | None = None) -> Status:
|
|
65
|
+
"""The analysis's reproducibility, from its record, its versions and its latest run."""
|
|
66
|
+
if record is None or not record.results:
|
|
67
|
+
return Status("unrecorded", "Not recorded yet", [
|
|
68
|
+
"The first run in which no box fails is kept as version 1: the record keeps the inputs' "
|
|
69
|
+
"fingerprints, the versions, the seeds and every result, so the numbers can be checked again later."])
|
|
70
|
+
latest = versions.latest if versions is not None else None
|
|
71
|
+
v = f"v{latest.number}" if latest is not None else "the record"
|
|
72
|
+
name = f"v{latest.number}" if latest is not None else "Recorded" # "v3 · reproducible"
|
|
73
|
+
changed = differences(record, workflow, folder)
|
|
74
|
+
if latest is not None:
|
|
75
|
+
now = take(workflow, None)
|
|
76
|
+
changed += setting_changes(latest.snapshot, now)
|
|
77
|
+
changed += [f"“{now.titles.get(n, n)}” was added." for n in now.settings if n not in latest.snapshot.settings]
|
|
78
|
+
changed += [f"“{latest.snapshot.titles.get(n, n)}” was removed."
|
|
79
|
+
for n in latest.snapshot.settings if n not in now.settings]
|
|
80
|
+
# A box that could not run here (a library is missing) is not a difference
|
|
81
|
+
# in its result: say why it could not be compared instead.
|
|
82
|
+
blocked = {node_id for node_id in record.results
|
|
83
|
+
if run is not None and run.result(node_id, 0) is not None and run.result(node_id, 0).status == BLOCKED}
|
|
84
|
+
uncompared = []
|
|
85
|
+
if blocked:
|
|
86
|
+
record = Record(**{**record.__dict__, "results": {k: v for k, v in record.results.items() if k not in blocked}})
|
|
87
|
+
uncompared = [f"“{workflow.title(n)}” could not run here ({run.result(n, 0).message}), so its "
|
|
88
|
+
"result was not compared." for n in sorted(blocked)]
|
|
89
|
+
changed += uncompared # not a reproduction either: say so, as a change
|
|
90
|
+
mismatches = check(workflow, record, run) if run is not None else []
|
|
91
|
+
compared = [node_id for node_id in record.results
|
|
92
|
+
if run is not None and run.result(node_id, 0) is not None
|
|
93
|
+
and run.result(node_id, 0).status == DONE]
|
|
94
|
+
count = len(changed)
|
|
95
|
+
plural = f"{count} change{'s' if count != 1 else ''}"
|
|
96
|
+
if mismatches and changed:
|
|
97
|
+
return Status("moved", f"The results differ from {v} after {plural}", changed + mismatches, v, count)
|
|
98
|
+
if mismatches:
|
|
99
|
+
return Status("differs", f"This run differs from {v}, with nothing changed", mismatches, v, 0)
|
|
100
|
+
if changed:
|
|
101
|
+
return Status("changed", f"{name} · {plural} since", changed, name, count)
|
|
102
|
+
if not compared:
|
|
103
|
+
return Status("unchecked", f"{name} · not run since", [
|
|
104
|
+
"Run ▶ and every result is checked against its recorded fingerprint."], name, 0)
|
|
105
|
+
return Status("reproduces", f"{name} · this run reproduces it", [
|
|
106
|
+
f"{len(compared)} result{'s' if len(compared) != 1 else ''} match the record's fingerprints, and "
|
|
107
|
+
"nothing the analysis depends on has changed."], name, 0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = ["Status", "status"]
|