openprocess 0.16.0__tar.gz → 0.16.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openprocess-0.16.0/openprocess.egg-info → openprocess-0.16.2}/PKG-INFO +8 -2
- {openprocess-0.16.0 → openprocess-0.16.2}/README.md +7 -1
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/__init__.py +1 -1
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/analysis/state_space.py +7 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/check.py +6 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/derivation.py +2 -1
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/figures.py +1 -1
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/report.py +8 -2
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/viewers.py +4 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/learn/answer_boxes.py +25 -5
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/learn/mode.py +8 -3
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/app.py +10 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/cpn_page.py +10 -5
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/dotted_chart.py +41 -6
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/instances.py +2 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/model_page.py +3 -1
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mcp/api.py +2 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/analysis.py +19 -5
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/conformance/alignments.py +28 -6
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/conformance/token_replay.py +36 -2
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/model/net.py +19 -0
- {openprocess-0.16.0 → openprocess-0.16.2/openprocess.egg-info}/PKG-INFO +8 -2
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_dotted_chart.py +25 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_exercises_gui.py +56 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_review_fixes.py +86 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_studio_gui.py +31 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/LICENSE +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/cpnpy/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/analysis/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/analysis/state_space_process.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/citation.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/cli.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/exercises/pack.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/box.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/compare.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/cpn.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/discover.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/filter.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/input.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/measure.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/output.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/predict.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/science.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/boxes/sweeps.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/changes.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/convert.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/datasets.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/explain.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/history.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/library.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/loading.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/parallel.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/provenance.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/record.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/report.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/reproducibility.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/runner.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/script.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/snapshots.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/studio.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/sweep.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/types.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/versions.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/flow/workflow.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/app.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/arc_editing.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/canvas.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/canvas.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/page.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/picker.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/stepper.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/templates.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/flow/versions.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/items.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/learn/concealment.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/panning.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/resources/openprocess-icon.png +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/__main__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/alignment_view.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/charts.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/cite.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/command_palette.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/compare_page.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/connect.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/connections.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/definition_view.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/derivation_view.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/documents.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/file_dialogs.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/filter_dialog.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/graph_builders.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/graph_view.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/loading.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/log_editor.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/log_page.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/markdown_view.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/mathtext.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/ml_highlighter.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/motion.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/net_comparison.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/notes_overlay.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/petri_page.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/provenance.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/regions_view.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/remote.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/review.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/sidebar.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/spaces.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/style.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/tool_icons.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/updates.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/widgets.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/workers.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/studio/workspace.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/theme.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/gui/tidy.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/io/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/io/cpn_reader.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/io/cpn_writer.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/answers.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/checks.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/computed.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/context.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/exam.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/exercise-packs.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/importer.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/notation.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/pack.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/learn/sheet.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mcp/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mcp/__main__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mcp/connecting.md +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mcp/install.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mcp/server.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/bpmn.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/cases.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/columns.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/compare_nets.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/conformance/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/conformance/quality.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/csv_import.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/definitions.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/dfg.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/discovery/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/discovery/alpha.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/discovery/heuristics.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/discovery/inductive.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/discovery/state_regions.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/dotted.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/drawing.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/filtering.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/footprint.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/invariants.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/layout.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/log.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/ocel.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/parallel.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/petrinet.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/playout.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/pm4py_bridge.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/pnml.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/processtree.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/regions.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/stats.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/structure.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/transition_system.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/mining/xes.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/ast_nodes.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/builtins.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/colorsets.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/errors.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/evaluator.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/lexer.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/multiset.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/parser.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/ml/values.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/model/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/model/declarations.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/model/examples.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/model/plain.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/packages.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/palette.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/parallel.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/references.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/sim/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/sim/binding.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/sim/export.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/sim/performance.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/sim/simulator.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/teaching/__init__.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/teaching/answers.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/teaching/checks.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/teaching/pack.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess/teaching/sheet.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess.egg-info/SOURCES.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess.egg-info/dependency_links.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess.egg-info/entry_points.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess.egg-info/requires.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/openprocess.egg-info/top_level.txt +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/pyproject.toml +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/setup.cfg +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_arc_editing.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_bpmn.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_citation.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_colorsets.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_columns.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_cpn_course_model.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_definitions.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_docs.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_flow.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_flow_gui.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_framework_contract.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_hierarchy.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_history.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_io.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_large_log_page.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_large_logs.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_learn.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_learn_gui.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_log_editing.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_mcp.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_mcp_gui.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_measure.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_mining.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_ml.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_multiset.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_ocel.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_packages.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_parallel_and_loading.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_parallel_boxes.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_performance.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_regions.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_rename.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_report.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_researcher_api.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_simulation.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_snapshots.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_state_space.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_studio_lazy.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_teaching.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_tidy.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_updates.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_wf_structure.py +0 -0
- {openprocess-0.16.0 → openprocess-0.16.2}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprocess
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.2
|
|
4
4
|
Summary: Process mining, Petri nets and workflows in one open app: every algorithm visible, every result reproducible, every step teachable
|
|
5
5
|
Author: Sam Struthers
|
|
6
6
|
License: MIT
|
|
@@ -742,6 +742,9 @@ course).
|
|
|
742
742
|
|
|
743
743
|
## For developers
|
|
744
744
|
|
|
745
|
+
Want to report a wrong result, add an algorithm or fix something? See
|
|
746
|
+
[**CONTRIBUTING.md**](CONTRIBUTING.md).
|
|
747
|
+
|
|
745
748
|
```bash
|
|
746
749
|
pytest -q # the whole suite, including GUI tests that run offscreen
|
|
747
750
|
openprocess --help # command line: check, simulate, state space, mining
|
|
@@ -1089,7 +1092,10 @@ Plainly stated:
|
|
|
1089
1092
|
each use its own copy of the page. Subpages used once, at any depth,
|
|
1090
1093
|
simulate.
|
|
1091
1094
|
2. **No CPN monitors or simulation reports**, and **no fairness
|
|
1092
|
-
properties** in the state space report (it says so).
|
|
1095
|
+
properties** in the state space report (it says so). **Transition
|
|
1096
|
+
priorities and code segments are kept but not run**: a model that uses
|
|
1097
|
+
them simulates differently from CPN Tools, and the Problems tab and the
|
|
1098
|
+
state space report list where.
|
|
1093
1099
|
3. **CPN ML is a large subset, not all of Standard ML**: no `exception`
|
|
1094
1100
|
declarations or `handle`, no `datatype` or `structure` declarations, and
|
|
1095
1101
|
characters are one-letter strings. See
|
|
@@ -700,6 +700,9 @@ course).
|
|
|
700
700
|
|
|
701
701
|
## For developers
|
|
702
702
|
|
|
703
|
+
Want to report a wrong result, add an algorithm or fix something? See
|
|
704
|
+
[**CONTRIBUTING.md**](CONTRIBUTING.md).
|
|
705
|
+
|
|
703
706
|
```bash
|
|
704
707
|
pytest -q # the whole suite, including GUI tests that run offscreen
|
|
705
708
|
openprocess --help # command line: check, simulate, state space, mining
|
|
@@ -1047,7 +1050,10 @@ Plainly stated:
|
|
|
1047
1050
|
each use its own copy of the page. Subpages used once, at any depth,
|
|
1048
1051
|
simulate.
|
|
1049
1052
|
2. **No CPN monitors or simulation reports**, and **no fairness
|
|
1050
|
-
properties** in the state space report (it says so).
|
|
1053
|
+
properties** in the state space report (it says so). **Transition
|
|
1054
|
+
priorities and code segments are kept but not run**: a model that uses
|
|
1055
|
+
them simulates differently from CPN Tools, and the Problems tab and the
|
|
1056
|
+
state space report list where.
|
|
1051
1057
|
3. **CPN ML is a large subset, not all of Standard ML**: no `exception`
|
|
1052
1058
|
declarations or `handle`, no `datatype` or `structure` declarations, and
|
|
1053
1059
|
characters are one-letter strings. See
|
|
@@ -48,7 +48,7 @@ from .sim.simulator import Simulator
|
|
|
48
48
|
#: The one place the version number is written (pyproject.toml, the update
|
|
49
49
|
#: check and packaging/build.py read it from here). A release is tagged
|
|
50
50
|
#: "v" + this, e.g. v0.2.0.
|
|
51
|
-
__version__ = "0.16.
|
|
51
|
+
__version__ = "0.16.2"
|
|
52
52
|
|
|
53
53
|
__all__ = [
|
|
54
54
|
"Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
|
|
@@ -451,6 +451,13 @@ class StateSpace:
|
|
|
451
451
|
if self.partial:
|
|
452
452
|
add(" WARNING: the properties below describe only the explored")
|
|
453
453
|
add(" fragment and are not proofs about the whole model.")
|
|
454
|
+
ignored = getattr(self.net, "warnings", [])
|
|
455
|
+
if ignored:
|
|
456
|
+
add(f" WARNING: {len(ignored)} transition priorities or code segments are not")
|
|
457
|
+
add(" implemented and were not used: this state space can differ")
|
|
458
|
+
add(" from the one CPN Tools computes.")
|
|
459
|
+
for issue in ignored[:max_listed]:
|
|
460
|
+
add(f" {issue.element_name or issue.element_id} ({issue.field_name})")
|
|
454
461
|
add("")
|
|
455
462
|
|
|
456
463
|
add("Boundedness Properties")
|
|
@@ -48,6 +48,12 @@ def check_fit(model: PetriNet, log: EventLog,
|
|
|
48
48
|
scores = Scores(model.name, {"fitness": fitness, "precision": precision(model, simple),
|
|
49
49
|
"generalisation": generalisation(replay), "simplicity": simplicity(model)})
|
|
50
50
|
scores.note = f"Checked against all {replay.trace_count} cases of {log.name} ({method.lower()})."
|
|
51
|
+
if replay.unknown_events:
|
|
52
|
+
missing = ", ".join(f"{a} ({n:,})" for a, n in replay.unknown_totals().most_common(5))
|
|
53
|
+
text = (f"{replay.unknown_events:,} events are of activities the model does not have: {missing}"
|
|
54
|
+
f"{', …' if len(replay.unknown_totals()) > 5 else ''}.")
|
|
55
|
+
flow.note(text + " In token replay each counts one missing and one remaining token.")
|
|
56
|
+
scores.note += " " + text
|
|
51
57
|
if method == "Alignments" and alignments.incomplete:
|
|
52
58
|
cases = sum(a.count for a in alignments.incomplete)
|
|
53
59
|
scores.note += (f" No optimal alignment was found for {len(alignments.incomplete)} variants "
|
|
@@ -94,7 +94,8 @@ def value_markdown(value: Any, limit: int = 50) -> str:
|
|
|
94
94
|
+ _md_table(["Variant", "Cases", "p", "c", "m", "r"], rows))
|
|
95
95
|
if isinstance(value, AlignmentResult):
|
|
96
96
|
rows = [[f"⟨{', '.join(a.trace)}⟩", a.count, a.cost, f"{a.fitness:.3f}"] for a in value.alignments[:limit]]
|
|
97
|
-
return (f"Fitness {
|
|
97
|
+
return (f"Fitness {'' if value.exact else 'at least '}{value.average_fitness:.4f}; "
|
|
98
|
+
f"{value.fitting_traces:,} of {value.trace_count:,} cases "
|
|
98
99
|
f"align without a deviation\n\n" + _md_table(["Variant", "Cases", "Cost", "Fitness"], rows))
|
|
99
100
|
if isinstance(value, OCEL):
|
|
100
101
|
return value.summary()
|
|
@@ -49,7 +49,7 @@ def tile_parts(value) -> tuple[str, str] | None:
|
|
|
49
49
|
if isinstance(value, ReplayResult):
|
|
50
50
|
return f"{value.fitness:.2f} fitness", f"{value.fitting_traces:,} of {value.trace_count:,} cases fit"
|
|
51
51
|
if isinstance(value, AlignmentResult):
|
|
52
|
-
return (f"{value.average_fitness:.2f} fitness",
|
|
52
|
+
return (f"{'' if value.exact else '≥ '}{value.average_fitness:.2f} fitness",
|
|
53
53
|
f"{value.fitting_traces:,} of {value.trace_count:,} cases align")
|
|
54
54
|
if isinstance(value, Text):
|
|
55
55
|
first = str(value).strip().splitlines()[0] if str(value).strip() else ""
|
|
@@ -293,8 +293,14 @@ class Section(QFrame):
|
|
|
293
293
|
if result is not None and result.status == DONE:
|
|
294
294
|
parts = key_figure(result.value)
|
|
295
295
|
if parts is not None:
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
figure, line = parts
|
|
297
|
+
if len(line) > 72:
|
|
298
|
+
line = line[:71].rstrip() + "…"
|
|
299
|
+
tile = StatTile("Key figure", figure, line)
|
|
300
|
+
# One line each, the tile as wide as they need: a wrapped line in a
|
|
301
|
+
# tile of fixed height was drawn over the figure above it.
|
|
302
|
+
tile.value.setWordWrap(False)
|
|
303
|
+
tile.sub.setWordWrap(False)
|
|
298
304
|
self.body_layout.addWidget(tile, 0, Qt.AlignLeft)
|
|
299
305
|
self.body_widget = page.result_view(node, result)
|
|
300
306
|
self.body_layout.addWidget(self.body_widget)
|
|
@@ -362,6 +362,10 @@ def result_widget(value, page=None, node=None) -> QWidget:
|
|
|
362
362
|
add(label(f"Fitness {value.fitness:.4f}: p = {value.produced}, c = {value.consumed}, "
|
|
363
363
|
f"m = {value.missing}, r = {value.remaining}; {value.fitting_traces} of "
|
|
364
364
|
f"{value.trace_count} cases fit.", "muted", wrap=True))
|
|
365
|
+
if value.unknown_events:
|
|
366
|
+
add(label(f"{value.unknown_events:,} events are of activities the model does not have ("
|
|
367
|
+
+ ", ".join(f"{a}: {n:,}" for a, n in value.unknown_totals().most_common(8))
|
|
368
|
+
+ "). Each counts one missing and one remaining token.", "muted", wrap=True))
|
|
365
369
|
elif isinstance(value, AlignmentResult):
|
|
366
370
|
from ..studio.alignment_view import AlignmentList, legend_items, projection
|
|
367
371
|
from ..studio.widgets import Legend, LegendSwatch
|
|
@@ -42,9 +42,13 @@ CAPTIONS = {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
#: Status of a task -> (chip text, status colour key).
|
|
45
|
+
#: The answer was shown before it was given: right now, but it does not count.
|
|
46
|
+
SHOWN = "shown"
|
|
47
|
+
|
|
45
48
|
STATUS = {
|
|
46
49
|
CORRECT: ("Correct", "good"), PARTIAL: ("Almost", "warning"),
|
|
47
50
|
INCORRECT: ("Not yet", "critical"), "done": ("Done", "good"),
|
|
51
|
+
SHOWN: ("Answer shown", "muted"),
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
|
|
@@ -771,6 +775,9 @@ class TaskCard(QFrame):
|
|
|
771
775
|
self.status: str | None = None
|
|
772
776
|
#: The share of the block's points the last check gave (for partial answers).
|
|
773
777
|
self.share: float = 0.0
|
|
778
|
+
#: *Show answer* was pressed before the answer was right: a right answer
|
|
779
|
+
#: after that is the shown one typed in, so it is not marked Correct.
|
|
780
|
+
self.revealed: bool = False
|
|
774
781
|
#: In an exam there is no hint, no answer, and Check only says if it is right.
|
|
775
782
|
self.exam = exam
|
|
776
783
|
layout = QVBoxLayout(self)
|
|
@@ -838,7 +845,7 @@ class TaskCard(QFrame):
|
|
|
838
845
|
# -- feedback ------------------------------------------------------------------------
|
|
839
846
|
def _edited(self) -> None:
|
|
840
847
|
# Feedback is about the answer as it was: it goes when you change it.
|
|
841
|
-
if self.status in (CORRECT, PARTIAL, INCORRECT) or not self.feedback.isHidden():
|
|
848
|
+
if self.status in (CORRECT, PARTIAL, INCORRECT, SHOWN) or not self.feedback.isHidden():
|
|
842
849
|
self.set_status(None)
|
|
843
850
|
self.feedback.setHidden(True)
|
|
844
851
|
self._clear_detail()
|
|
@@ -876,8 +883,12 @@ class TaskCard(QFrame):
|
|
|
876
883
|
status = result.status
|
|
877
884
|
self.share = float(getattr(result, "share", 0.0) or 0.0)
|
|
878
885
|
icon = {CORRECT: "✓", PARTIAL: "◐", INCORRECT: "✕"}.get(status, "")
|
|
879
|
-
|
|
880
|
-
|
|
886
|
+
message = escape(result.message)
|
|
887
|
+
if self.revealed and status in (CORRECT, PARTIAL):
|
|
888
|
+
# Still told whether it is right, but it earns nothing and is not "done".
|
|
889
|
+
status, self.share = SHOWN, 0.0
|
|
890
|
+
message += " <i>The answer was shown first, so this does not count.</i>"
|
|
891
|
+
self.feedback.setText(f"<b>{icon}</b> {message}" if icon else message)
|
|
881
892
|
self.feedback.setProperty("state", STATUS.get(status, ("", ""))[1])
|
|
882
893
|
self._repolish(self.feedback)
|
|
883
894
|
self.feedback.setHidden(False)
|
|
@@ -912,8 +923,17 @@ class TaskCard(QFrame):
|
|
|
912
923
|
self.solution_label.body.set_markdown(text)
|
|
913
924
|
self.solution_label.setHidden(False)
|
|
914
925
|
self.answer_button.setHidden(True)
|
|
915
|
-
if self.task.type == "open"
|
|
916
|
-
self.
|
|
926
|
+
if self.task.type == "open":
|
|
927
|
+
if self.status != "done":
|
|
928
|
+
self.assess_row.setHidden(False)
|
|
929
|
+
elif self.status not in (CORRECT, "done") and not self.revealed:
|
|
930
|
+
# An answer that was already right stays right; anything else was not
|
|
931
|
+
# answered before the answer was seen. (An open answer is judged by
|
|
932
|
+
# yourself against the model answer, so showing it is the point.)
|
|
933
|
+
self.revealed = True
|
|
934
|
+
if self.status == PARTIAL:
|
|
935
|
+
self.set_status(None)
|
|
936
|
+
self.changed.emit(self.task)
|
|
917
937
|
|
|
918
938
|
def assess(self, had_it: bool) -> None:
|
|
919
939
|
self.assess_row.setHidden(True)
|
|
@@ -59,7 +59,7 @@ from ...learn.checks import check as check_answer
|
|
|
59
59
|
from ...learn.checks import expected, footprint_of, model_answer_text
|
|
60
60
|
from ...learn.exam import ExamState, marks_csv
|
|
61
61
|
from ...learn.pack import Exercise, Pack, load_pack
|
|
62
|
-
from .answer_boxes import TaskCard, debounce, make_editor
|
|
62
|
+
from .answer_boxes import SHOWN, TaskCard, debounce, make_editor
|
|
63
63
|
from .concealment import Concealment
|
|
64
64
|
from ..studio.markdown_view import MarkdownLabel
|
|
65
65
|
from ..studio.widgets import SegmentedControl, button, hbox, label, scroll
|
|
@@ -802,7 +802,7 @@ class ExerciseView(QWidget):
|
|
|
802
802
|
self._workflow_timer = debounce(self, WORKFLOW_SAVE_DELAY, self._save_workflow)
|
|
803
803
|
self._workflow_timer.start()
|
|
804
804
|
card = self.cards.get(task.id)
|
|
805
|
-
if card is not None and card.status in (CORRECT, PARTIAL, INCORRECT):
|
|
805
|
+
if card is not None and card.status in (CORRECT, PARTIAL, INCORRECT, SHOWN):
|
|
806
806
|
card.editor.changed.emit() # the old verdict no longer applies
|
|
807
807
|
|
|
808
808
|
def _workflow_saved(self, task) -> None:
|
|
@@ -839,7 +839,7 @@ class ExerciseView(QWidget):
|
|
|
839
839
|
self._net_timers[task.id] = timer
|
|
840
840
|
timer.start()
|
|
841
841
|
card = self.cards.get(task.id)
|
|
842
|
-
if card is not None and card.status in (CORRECT, PARTIAL, INCORRECT):
|
|
842
|
+
if card is not None and card.status in (CORRECT, PARTIAL, INCORRECT, SHOWN):
|
|
843
843
|
card.editor.changed.emit() # the old verdict no longer applies
|
|
844
844
|
|
|
845
845
|
def _save_net(self, page, target: Path) -> None:
|
|
@@ -1004,6 +1004,7 @@ class ExerciseView(QWidget):
|
|
|
1004
1004
|
saved = self.progress.get(card.task.id, {})
|
|
1005
1005
|
if card.task.type not in ("net", "workflow") and "answer" in saved:
|
|
1006
1006
|
card.editor.set_value(saved["answer"])
|
|
1007
|
+
card.revealed = bool(saved.get("shown"))
|
|
1007
1008
|
status = saved.get("status")
|
|
1008
1009
|
if status:
|
|
1009
1010
|
card.set_status(status)
|
|
@@ -1018,6 +1019,10 @@ class ExerciseView(QWidget):
|
|
|
1018
1019
|
entry["status"] = card.status
|
|
1019
1020
|
else:
|
|
1020
1021
|
entry.pop("status", None)
|
|
1022
|
+
if card.revealed:
|
|
1023
|
+
entry["shown"] = True
|
|
1024
|
+
else:
|
|
1025
|
+
entry.pop("shown", None)
|
|
1021
1026
|
if card.status == PARTIAL and card.share:
|
|
1022
1027
|
entry["share"] = round(card.share, 4)
|
|
1023
1028
|
else:
|
|
@@ -4261,6 +4261,16 @@ class StudioWindow(QMainWindow):
|
|
|
4261
4261
|
self.statusBar().showMessage(message, 8000)
|
|
4262
4262
|
|
|
4263
4263
|
try:
|
|
4264
|
+
if isinstance(document, WorkflowDocument):
|
|
4265
|
+
# Changed by Claude (through the MCP server) or another copy of the
|
|
4266
|
+
# app: the boxes, versions and record are all read again.
|
|
4267
|
+
from ...flow.record import load_all
|
|
4268
|
+
document.workflow, document.record, document.versions = load_all(path, self.library())
|
|
4269
|
+
document.dirty = False
|
|
4270
|
+
self._remember_identity(document)
|
|
4271
|
+
self._replace_page(document)
|
|
4272
|
+
self.statusBar().showMessage(message, 8000)
|
|
4273
|
+
return
|
|
4264
4274
|
if isinstance(document, TransitionSystemDocument):
|
|
4265
4275
|
content = self._read_text_file(path)
|
|
4266
4276
|
document.ts = content.ts
|
|
@@ -823,7 +823,11 @@ class CpnPage(QWidget):
|
|
|
823
823
|
def _update_sim_status(self) -> None:
|
|
824
824
|
if not self.simulating:
|
|
825
825
|
problems = len(self.net.errors)
|
|
826
|
-
self.
|
|
826
|
+
ignored = len(self.net.warnings)
|
|
827
|
+
self.sim_status.setText((f"⚠ compiles, but {ignored} priorit"
|
|
828
|
+
f"{'y or code segment is' if ignored == 1 else 'ies or code segments are'}"
|
|
829
|
+
" not used — see the Problems tab" if ignored else "✓ compiles")
|
|
830
|
+
if not problems else
|
|
827
831
|
f"⚠ {problems} problem{'s' * (problems != 1)} — see "
|
|
828
832
|
"the Problems tab")
|
|
829
833
|
return
|
|
@@ -909,18 +913,19 @@ class CpnPage(QWidget):
|
|
|
909
913
|
|
|
910
914
|
def _fill_problems(self) -> None:
|
|
911
915
|
errors = list(self.net.errors)
|
|
912
|
-
|
|
916
|
+
warnings = list(self.net.warnings)
|
|
917
|
+
count = len(errors) + len(warnings)
|
|
913
918
|
self.inspector_tabs.buttons[2].setText(f"Problems ({count})" if count else "Problems")
|
|
914
919
|
card = self.problems_card
|
|
915
920
|
card.clear()
|
|
916
921
|
if not errors:
|
|
917
922
|
card.add(Verdict("Model compiles", "good", "Every declaration and inscription "
|
|
918
923
|
"parsed. The model is ready to simulate."))
|
|
919
|
-
|
|
920
|
-
for issue in errors[:200]:
|
|
924
|
+
for issue in (errors + warnings)[:200]:
|
|
921
925
|
row = QWidget()
|
|
922
926
|
title = f"{issue.element_name or 'Declarations'} — {issue.field_name}"
|
|
923
|
-
layout = hbox(Verdict(title, "critical"
|
|
927
|
+
layout = hbox(Verdict(title, "critical" if issue in errors else "warning",
|
|
928
|
+
issue.message), spacing=6)
|
|
924
929
|
if issue.element_id:
|
|
925
930
|
layout.addWidget(button("Show", lambda _=False, i=issue.element_id:
|
|
926
931
|
self.reveal_element(i), kind="ghost"), 0, Qt.AlignTop)
|
|
@@ -73,6 +73,7 @@ SHAPES = ["circle", "square", "triangle", "diamond", "cross", "star"]
|
|
|
73
73
|
#: log stays a picture of milliseconds, not seconds. Shapes, rings and the
|
|
74
74
|
#: lines between a case's events come back as soon as a zoom shows fewer.
|
|
75
75
|
BATCH_FROM = 40_000
|
|
76
|
+
STACK_ALPHA = 0.72 # opacity of a dot; a stack of dots shows darker
|
|
76
77
|
#: Above this many events the dots are worked out on a worker thread.
|
|
77
78
|
BACKGROUND_FROM = 200_000
|
|
78
79
|
#: When drawing the picture took longer than this, a pan or zoom first shows
|
|
@@ -381,6 +382,15 @@ class DottedChart(QWidget):
|
|
|
381
382
|
painter.end()
|
|
382
383
|
return pixmap
|
|
383
384
|
|
|
385
|
+
def dot_radius(self, rect: QRectF) -> float:
|
|
386
|
+
"""The radius a dot is drawn with: the size the rows allow (4 px
|
|
387
|
+
when they are tall, down to 1.6 px when a row is a hairline), scaled
|
|
388
|
+
by the Dot size setting (4 is 1×), so the slider always does
|
|
389
|
+
something and a dense chart stays readable."""
|
|
390
|
+
row_h = rect.height() / max(self.vy1 - self.vy0, 1e-9)
|
|
391
|
+
fitting = max(1.6, min(4.0, row_h * 0.45))
|
|
392
|
+
return max(1.0, fitting * self.data.settings.dot_size / 4.0)
|
|
393
|
+
|
|
384
394
|
def _visible(self, candidates: np.ndarray) -> np.ndarray:
|
|
385
395
|
"""The dots of ``candidates`` (dots in the x window) that are in the
|
|
386
396
|
row window and whose colour the legend shows."""
|
|
@@ -402,12 +412,7 @@ class DottedChart(QWidget):
|
|
|
402
412
|
rect = self.plot_rect()
|
|
403
413
|
self._paint_axes(painter, rect, t, s.grid_lines)
|
|
404
414
|
|
|
405
|
-
|
|
406
|
-
# The dot size the rows allow (4 px when they are tall, down to 1.6 px
|
|
407
|
-
# when a row is a hairline), scaled by the Dot size setting (4 is 1×):
|
|
408
|
-
# the slider always does something, and a dense chart stays readable.
|
|
409
|
-
fitting = max(1.6, min(4.0, row_h * 0.45))
|
|
410
|
-
radius = max(1.0, fitting * s.dot_size / 4.0)
|
|
415
|
+
radius = self.dot_radius(rect)
|
|
411
416
|
# A ring in the surface colour sets a dot apart from its neighbours;
|
|
412
417
|
# drawn on every dot it erases overlapping ones into slivers, so it is
|
|
413
418
|
# kept for the selected case alone, which sits on top.
|
|
@@ -423,6 +428,13 @@ class DottedChart(QWidget):
|
|
|
423
428
|
self._paint_dense(painter, rect, colours, faded, radius, visible)
|
|
424
429
|
on_top = visible[d.case[visible] == selected] if selected >= 0 else []
|
|
425
430
|
else:
|
|
431
|
+
if selected < 0:
|
|
432
|
+
# Dots a little translucent, so a stack of events drawn on
|
|
433
|
+
# the same spot shows darker than a single one. The dense
|
|
434
|
+
# picture sets each pixel once, so it stays opaque; the
|
|
435
|
+
# selected case is opaque to stand out.
|
|
436
|
+
for colour in colours:
|
|
437
|
+
colour.setAlphaF(STACK_ALPHA)
|
|
426
438
|
if s.connect:
|
|
427
439
|
self._paint_connections(painter, rect, visible, t)
|
|
428
440
|
# Unselected first, the selected case last so it sits on top.
|
|
@@ -634,6 +646,16 @@ class DottedChart(QWidget):
|
|
|
634
646
|
nearest = int(distance.argmin())
|
|
635
647
|
return int(near[nearest]) if distance[nearest] <= tolerance else None
|
|
636
648
|
|
|
649
|
+
def stacked_with(self, i: int) -> np.ndarray:
|
|
650
|
+
"""The shown dots drawn over dot ``i``: those on its row whose centre
|
|
651
|
+
is within a dot's width of it on screen, ``i`` itself first."""
|
|
652
|
+
d = self.data
|
|
653
|
+
rect = self.plot_rect()
|
|
654
|
+
reach = 2 * self.dot_radius(rect) / rect.width() * (self.vx1 - self.vx0)
|
|
655
|
+
near = d.between(d.xs[i] - reach, d.xs[i] + reach)
|
|
656
|
+
near = near[(d.rows[near] == d.rows[i]) & d.shown(self.hidden_colours)[d.colour[near]]]
|
|
657
|
+
return np.concatenate(([i], near[near != i]))
|
|
658
|
+
|
|
637
659
|
def describe_dot(self, i: int) -> str:
|
|
638
660
|
d = self.data
|
|
639
661
|
event = d.event(i)
|
|
@@ -646,6 +668,19 @@ class DottedChart(QWidget):
|
|
|
646
668
|
if key not in ("concept:name", "time:timestamp"):
|
|
647
669
|
text = str(value)
|
|
648
670
|
lines.append(f"{key}: {text[:60] + '…' if len(text) > 60 else text}")
|
|
671
|
+
stack = self.stacked_with(i)
|
|
672
|
+
if len(stack) > 1:
|
|
673
|
+
# A pile of dots looks like one: say how many, and over what span.
|
|
674
|
+
cases = len(set(d.case[stack].tolist()))
|
|
675
|
+
text = f"<i>{len(stack) - 1} more event{'s' if len(stack) > 2 else ''} under this dot"
|
|
676
|
+
if cases > 1:
|
|
677
|
+
text += f" ({cases} cases)"
|
|
678
|
+
stamps = [d.event(int(j)).timestamp for j in stack]
|
|
679
|
+
if all(stamps):
|
|
680
|
+
first, last = min(stamps), max(stamps)
|
|
681
|
+
if last > first:
|
|
682
|
+
text += f", spanning {format_duration((last - first).total_seconds())}"
|
|
683
|
+
lines.append(text + ". Drag to zoom in.</i>")
|
|
649
684
|
return "<br>".join(lines)
|
|
650
685
|
|
|
651
686
|
# -- interaction ------------------------------------------------------------------
|
|
@@ -237,6 +237,8 @@ def properties(props: PropertyReport, key: str) -> list[str]:
|
|
|
237
237
|
m = marking(net, graph.states[state])
|
|
238
238
|
return [f"{m0} {firing(net, graph.path_to(state))} {m}",
|
|
239
239
|
f"\\forall t \\in T \\colon t \\text{{ is not enabled in }} {m}"]
|
|
240
|
+
if props.deadlock_free is None:
|
|
241
|
+
return ["\\text{not decided: the state space is unbounded or too large}"]
|
|
240
242
|
return [f"\\forall M \\in {reach} \\; \\exists t \\in T \\colon t "
|
|
241
243
|
"\\text{ enabled in } M"]
|
|
242
244
|
if key == "dead_transition":
|
|
@@ -627,7 +627,9 @@ class ModelPage(ConcealsResults, QWidget):
|
|
|
627
627
|
f"log-level {alignments.log_fitness:.3f}")
|
|
628
628
|
tiles["tbr"].set(f"{replay.fitness:.3f}",
|
|
629
629
|
f"m {replay.missing:,} · r {replay.remaining:,} · "
|
|
630
|
-
f"c {replay.consumed:,} · p {replay.produced:,}"
|
|
630
|
+
f"c {replay.consumed:,} · p {replay.produced:,}"
|
|
631
|
+
+ (f" · {replay.unknown_events:,} events not in the model"
|
|
632
|
+
if replay.unknown_events else ""))
|
|
631
633
|
tiles["precision"].set(f"{prec:.3f}", "escaping edges (ETC)")
|
|
632
634
|
tiles["generalisation"].set(f"{gen:.3f}", "token-based")
|
|
633
635
|
tiles["simplicity"].set(f"{simp:.3f}", "arc degree")
|
|
@@ -1020,6 +1020,7 @@ def cpn_info(path: str) -> dict:
|
|
|
1020
1020
|
compile problems."""
|
|
1021
1021
|
net = _cpn(path)
|
|
1022
1022
|
return {"name": net.name, "problems": [str(e) for e in net.errors],
|
|
1023
|
+
"not_implemented": [str(w) for w in net.warnings],
|
|
1023
1024
|
"pages": [{"name": p.name, "places": len(p.places), "transitions": len(p.transitions),
|
|
1024
1025
|
"arcs": len(p.arcs)} for p in net.pages],
|
|
1025
1026
|
"colour_sets": {n: type(c).__name__ for n, c in sorted(net.declarations.colour_sets.items())},
|
|
@@ -1041,6 +1042,7 @@ def cpn_simulate(path: str, steps: int = 50, seed: int = 0) -> dict:
|
|
|
1041
1042
|
return {"steps": taken, "asked": steps, "clock": simulator.clock, "dead": taken < steps,
|
|
1042
1043
|
"log": [record.describe(net) for record in simulator.log][-200:],
|
|
1043
1044
|
"final_marking": simulator.marking.describe(net),
|
|
1045
|
+
"not_implemented": [str(w) for w in net.warnings],
|
|
1044
1046
|
"cite": citations("openprocess.sim"),
|
|
1045
1047
|
"reproduce": f"openprocess simulate {_q(path)} --steps {steps} --seed {seed}"}
|
|
1046
1048
|
|
|
@@ -338,8 +338,18 @@ class PropertyReport:
|
|
|
338
338
|
return self.bounded and self.bound <= 1
|
|
339
339
|
|
|
340
340
|
@property
|
|
341
|
-
def deadlock_free(self) -> bool:
|
|
342
|
-
|
|
341
|
+
def deadlock_free(self) -> bool | None:
|
|
342
|
+
"""False with a dead marking in hand (in a coverability graph too: a
|
|
343
|
+
dead node stands for reachable markings in which nothing is enabled).
|
|
344
|
+
None when no dead marking was found but the search cannot prove there
|
|
345
|
+
is none: a coverability graph's ω enables what a finite marking may
|
|
346
|
+
not, so it can hide a deadlock, and a truncated graph has unexplored
|
|
347
|
+
markings."""
|
|
348
|
+
if self.dead_markings:
|
|
349
|
+
return False
|
|
350
|
+
if self.graph.has_omega or self.graph.truncated:
|
|
351
|
+
return None
|
|
352
|
+
return True
|
|
343
353
|
|
|
344
354
|
@property
|
|
345
355
|
def terminating(self) -> bool | None:
|
|
@@ -375,8 +385,12 @@ class PropertyReport:
|
|
|
375
385
|
else "some run can go on forever"
|
|
376
386
|
+ (" (the net is unbounded)" if self.graph.has_omega
|
|
377
387
|
else ": the reachability graph has a cycle")))
|
|
378
|
-
|
|
379
|
-
|
|
388
|
+
deadlock_free = self.deadlock_free
|
|
389
|
+
rows.append(("Deadlock-free", deadlock_free,
|
|
390
|
+
"every reachable marking enables something" if deadlock_free
|
|
391
|
+
else ("not decided: the net is unbounded, and the coverability graph can "
|
|
392
|
+
"hide a deadlock" if self.graph.has_omega
|
|
393
|
+
else "not decided (truncated state space)") if deadlock_free is None
|
|
380
394
|
else f"{len(self.dead_markings)} dead marking(s), e.g. "
|
|
381
395
|
+ self.graph.states[self.dead_markings[0]].describe(net)
|
|
382
396
|
+ " via " + self.graph.describe_path(self.dead_markings[0])))
|
|
@@ -672,7 +686,7 @@ class ShortCircuitReport:
|
|
|
672
686
|
return self.properties.safe
|
|
673
687
|
|
|
674
688
|
@property
|
|
675
|
-
def deadlock_free(self) -> bool:
|
|
689
|
+
def deadlock_free(self) -> bool | None:
|
|
676
690
|
return self.properties.deadlock_free
|
|
677
691
|
|
|
678
692
|
@property
|
|
@@ -119,6 +119,15 @@ class AlignmentResult:
|
|
|
119
119
|
empty_trace_cost: int = 0
|
|
120
120
|
#: What the reader should know: searches that were cut short, and why.
|
|
121
121
|
notes: list[str] = field(default_factory=list)
|
|
122
|
+
#: False when the search for the model's shortest run gave up: the
|
|
123
|
+
#: empty-trace cost is then unknown (counted as 0), which makes every
|
|
124
|
+
#: fitness a lower bound. (Named as a working value so that a result's
|
|
125
|
+
#: fingerprint is what it was; ``notes`` says so when it is False.)
|
|
126
|
+
_empty_trace_complete: bool = True
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def empty_trace_complete(self) -> bool:
|
|
130
|
+
return self._empty_trace_complete
|
|
122
131
|
|
|
123
132
|
@property
|
|
124
133
|
def trace_count(self) -> int:
|
|
@@ -129,16 +138,25 @@ class AlignmentResult:
|
|
|
129
138
|
"""The variants whose optimal alignment was not found (state limit)."""
|
|
130
139
|
return [a for a in self.alignments if not a.complete]
|
|
131
140
|
|
|
141
|
+
@property
|
|
142
|
+
def exact(self) -> bool:
|
|
143
|
+
"""Was an optimal alignment found for every variant and for the empty
|
|
144
|
+
trace? If not, the fitness values are lower bounds, not the fitness."""
|
|
145
|
+
return self.empty_trace_complete and not self.incomplete
|
|
146
|
+
|
|
132
147
|
@property
|
|
133
148
|
def average_fitness(self) -> float:
|
|
134
|
-
"""Mean of the per-trace fitness values (weighted by frequency).
|
|
149
|
+
"""Mean of the per-trace fitness values (weighted by frequency). A
|
|
150
|
+
variant whose search gave up counts as 0: a lower bound (see :attr:`exact`)."""
|
|
135
151
|
total = self.trace_count
|
|
136
152
|
return sum(a.fitness * a.count for a in self.alignments) / total if total else 1.0
|
|
137
153
|
|
|
138
154
|
@property
|
|
139
155
|
def log_fitness(self) -> float:
|
|
140
|
-
"""1 − total cost / total worst-case cost.
|
|
141
|
-
|
|
156
|
+
"""1 − total cost / total worst-case cost. A variant whose search gave
|
|
157
|
+
up counts its worst case, as it counts 0 in :attr:`average_fitness`."""
|
|
158
|
+
cost = sum((a.cost if a.complete else len(a.trace) + self.empty_trace_cost) * a.count
|
|
159
|
+
for a in self.alignments)
|
|
142
160
|
worst = sum((len(a.trace) + self.empty_trace_cost) * a.count for a in self.alignments)
|
|
143
161
|
return 1 - cost / worst if worst else 1.0
|
|
144
162
|
|
|
@@ -307,7 +325,7 @@ def align_log(net: PetriNet, log: SimpleLog, max_states: int = MAX_STATES,
|
|
|
307
325
|
from .. import parallel
|
|
308
326
|
compiled = _CompiledNet(net)
|
|
309
327
|
empty = align_trace(net, (), 1, max_states, compiled)
|
|
310
|
-
result = AlignmentResult(net, empty_trace_cost=empty.cost)
|
|
328
|
+
result = AlignmentResult(net, empty_trace_cost=empty.cost, _empty_trace_complete=empty.complete)
|
|
311
329
|
variants = sorted(log.items(), key=lambda item: (-item[1], item[0]))
|
|
312
330
|
count = parallel.workers() if workers is None else workers
|
|
313
331
|
if parallel.worth_it(variants, count, "align"):
|
|
@@ -318,7 +336,9 @@ def align_log(net: PetriNet, log: SimpleLog, max_states: int = MAX_STATES,
|
|
|
318
336
|
alignment = searched[number - 1] if searched is not None else align_trace(net, trace, count, max_states,
|
|
319
337
|
compiled)
|
|
320
338
|
worst = len(trace) + empty.cost
|
|
321
|
-
|
|
339
|
+
# max: with the empty-trace cost unknown (counted as 0) the worst case is
|
|
340
|
+
# too small, and the value must stay a lower bound of the fitness.
|
|
341
|
+
alignment.fitness = max(0.0, 1 - alignment.cost / worst) if worst else 1.0
|
|
322
342
|
if not alignment.complete:
|
|
323
343
|
alignment.fitness = 0.0
|
|
324
344
|
result.alignments.append(alignment)
|
|
@@ -340,7 +360,9 @@ def align_log(net: PetriNet, log: SimpleLog, max_states: int = MAX_STATES,
|
|
|
340
360
|
f"variants ({cases:,} cases): the search stopped at {max_states:,} states. Their "
|
|
341
361
|
f"cost is unknown and their fitness is counted as 0."
|
|
342
362
|
if result.incomplete else
|
|
343
|
-
f"The search for the model's shortest run stopped at {max_states:,} states
|
|
363
|
+
f"The search for the model's shortest run stopped at {max_states:,} states, so the "
|
|
364
|
+
f"worst-case cost every fitness is measured against is too small: each fitness "
|
|
365
|
+
f"is a lower bound.")
|
|
344
366
|
reason = _unbounded_reason(net)
|
|
345
367
|
if reason:
|
|
346
368
|
result.notes.append(reason)
|
|
@@ -34,6 +34,19 @@ through silent moves, then the one missing the fewest tokens. These are
|
|
|
34
34
|
heuristics -- token replay is fast but not always optimal, which is why
|
|
35
35
|
alignments exist (see :mod:`alignments`).
|
|
36
36
|
|
|
37
|
+
Activities the model does not have
|
|
38
|
+
----------------------------------
|
|
39
|
+
The book replays logs whose activities all occur in the model. A real log
|
|
40
|
+
often has more. Skipping such an event would count it as if it fitted (a
|
|
41
|
+
trace of nothing but unknown activities would score 1), so it is counted as
|
|
42
|
+
what it is, something the log did that the model cannot do: the event is
|
|
43
|
+
replayed as a transition of its own with one input and one output place that
|
|
44
|
+
nothing else touches. Its input token is missing and its output token
|
|
45
|
+
remains, so each such event adds 1 to each of p, c, m and r. This is
|
|
46
|
+
OpenProcess's convention, not a published one; ``unknown_events`` on the
|
|
47
|
+
result says how many events it applied to, and the tokens are counted under
|
|
48
|
+
the activity (they belong to no place of the model).
|
|
49
|
+
|
|
37
50
|
Diagnostics
|
|
38
51
|
-----------
|
|
39
52
|
Per place we keep the total missing and remaining tokens, and per transition
|
|
@@ -59,7 +72,9 @@ class TraceReplay:
|
|
|
59
72
|
consumed: int
|
|
60
73
|
missing: int
|
|
61
74
|
remaining: int
|
|
62
|
-
#:
|
|
75
|
+
#: the activity of every event the model has no transition for, in trace
|
|
76
|
+
#: order; each counts one missing and one remaining token (see the module's
|
|
77
|
+
#: "Activities the model does not have")
|
|
63
78
|
unknown_activities: list[str] = field(default_factory=list)
|
|
64
79
|
#: indices of events whose transition was not enabled
|
|
65
80
|
problem_events: list[int] = field(default_factory=list)
|
|
@@ -120,6 +135,19 @@ class ReplayResult:
|
|
|
120
135
|
def trace_count(self) -> int:
|
|
121
136
|
return sum(t.count for t in self.traces)
|
|
122
137
|
|
|
138
|
+
@property
|
|
139
|
+
def unknown_events(self) -> int:
|
|
140
|
+
"""Events whose activity the model does not have, frequency-weighted."""
|
|
141
|
+
return sum(len(t.unknown_activities) * t.count for t in self.traces)
|
|
142
|
+
|
|
143
|
+
def unknown_totals(self) -> Counter:
|
|
144
|
+
"""Those events per activity, frequency-weighted."""
|
|
145
|
+
totals: Counter = Counter()
|
|
146
|
+
for t in self.traces:
|
|
147
|
+
for activity in t.unknown_activities:
|
|
148
|
+
totals[activity] += t.count
|
|
149
|
+
return totals
|
|
150
|
+
|
|
123
151
|
def place_totals(self) -> tuple[Counter, Counter]:
|
|
124
152
|
"""(missing, remaining) per place, frequency-weighted."""
|
|
125
153
|
missing, remaining = Counter(), Counter()
|
|
@@ -368,11 +396,17 @@ def replay_trace(net: PetriNet, trace: tuple[str, ...], count: int = 1,
|
|
|
368
396
|
result.fired[transition] += 1
|
|
369
397
|
return current - pre + post
|
|
370
398
|
|
|
399
|
+
unknown_remaining = 0
|
|
371
400
|
for index, activity in enumerate(trace):
|
|
372
401
|
candidates = by_label.get(activity)
|
|
373
402
|
if not candidates:
|
|
403
|
+
# No transition for it: one token missing to do it, one left behind.
|
|
374
404
|
result.unknown_activities.append(activity)
|
|
375
405
|
result.problem_events.append(index)
|
|
406
|
+
result.produced += 1
|
|
407
|
+
result.consumed += 1
|
|
408
|
+
result.missing += 1
|
|
409
|
+
unknown_remaining += 1
|
|
376
410
|
continue
|
|
377
411
|
chosen: str | None = next((t for t in candidates if net.is_enabled(marking, t)), None)
|
|
378
412
|
if chosen is None:
|
|
@@ -408,7 +442,7 @@ def replay_trace(net: PetriNet, trace: tuple[str, ...], count: int = 1,
|
|
|
408
442
|
marking = marking + short
|
|
409
443
|
result.consumed += final.total
|
|
410
444
|
leftover = marking - final
|
|
411
|
-
result.remaining = leftover.total
|
|
445
|
+
result.remaining = leftover.total + unknown_remaining
|
|
412
446
|
result.remaining_by_place.update(dict(leftover.items()))
|
|
413
447
|
return result
|
|
414
448
|
|