openprocess 0.16.1__tar.gz → 0.16.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openprocess-0.16.1/openprocess.egg-info → openprocess-0.16.2}/PKG-INFO +8 -2
- {openprocess-0.16.1 → openprocess-0.16.2}/README.md +7 -1
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/__init__.py +1 -1
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/analysis/state_space.py +7 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/check.py +6 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/derivation.py +2 -1
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/figures.py +1 -1
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/report.py +8 -2
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/viewers.py +4 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/learn/answer_boxes.py +25 -5
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/learn/mode.py +8 -3
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/cpn_page.py +10 -5
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/instances.py +2 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/model_page.py +3 -1
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/api.py +2 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/analysis.py +19 -5
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/conformance/alignments.py +28 -6
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/conformance/token_replay.py +36 -2
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/net.py +19 -0
- {openprocess-0.16.1 → openprocess-0.16.2/openprocess.egg-info}/PKG-INFO +8 -2
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_exercises_gui.py +56 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_review_fixes.py +86 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/LICENSE +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/cpnpy/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/analysis/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/analysis/state_space_process.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/citation.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/cli.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/exercises/pack.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/box.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/compare.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/cpn.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/discover.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/filter.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/input.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/measure.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/output.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/predict.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/science.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/boxes/sweeps.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/changes.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/convert.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/datasets.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/explain.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/history.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/library.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/loading.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/parallel.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/provenance.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/record.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/report.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/reproducibility.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/runner.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/script.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/snapshots.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/studio.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/sweep.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/types.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/versions.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/flow/workflow.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/app.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/arc_editing.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/canvas.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/canvas.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/page.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/picker.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/stepper.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/templates.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/flow/versions.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/items.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/learn/concealment.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/panning.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/resources/openprocess-icon.png +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/__main__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/alignment_view.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/app.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/charts.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/cite.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/command_palette.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/compare_page.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/connect.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/connections.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/definition_view.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/derivation_view.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/documents.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/dotted_chart.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/file_dialogs.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/filter_dialog.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/graph_builders.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/graph_view.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/loading.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/log_editor.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/log_page.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/markdown_view.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/mathtext.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/ml_highlighter.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/motion.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/net_comparison.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/notes_overlay.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/petri_page.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/provenance.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/regions_view.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/remote.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/review.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/sidebar.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/spaces.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/style.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/tool_icons.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/updates.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/widgets.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/workers.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/studio/workspace.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/theme.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/gui/tidy.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/io/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/io/cpn_reader.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/io/cpn_writer.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/answers.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/checks.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/computed.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/context.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/exam.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/exercise-packs.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/importer.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/notation.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/pack.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/learn/sheet.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/__main__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/connecting.md +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/install.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mcp/server.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/bpmn.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/cases.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/columns.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/compare_nets.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/conformance/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/conformance/quality.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/csv_import.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/definitions.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/dfg.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/alpha.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/heuristics.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/inductive.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/discovery/state_regions.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/dotted.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/drawing.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/filtering.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/footprint.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/invariants.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/layout.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/log.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/ocel.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/parallel.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/petrinet.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/playout.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/pm4py_bridge.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/pnml.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/processtree.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/regions.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/stats.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/structure.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/transition_system.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/mining/xes.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/ast_nodes.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/builtins.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/colorsets.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/errors.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/evaluator.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/lexer.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/multiset.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/parser.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/ml/values.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/declarations.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/examples.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/model/plain.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/packages.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/palette.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/parallel.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/references.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/binding.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/export.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/performance.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/sim/simulator.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/__init__.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/answers.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/checks.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/pack.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess/teaching/sheet.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/SOURCES.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/dependency_links.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/entry_points.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/requires.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/openprocess.egg-info/top_level.txt +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/pyproject.toml +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/setup.cfg +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_arc_editing.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_bpmn.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_citation.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_colorsets.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_columns.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_cpn_course_model.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_definitions.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_docs.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_dotted_chart.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_flow.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_flow_gui.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_framework_contract.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_hierarchy.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_history.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_io.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_large_log_page.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_large_logs.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_learn.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_learn_gui.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_log_editing.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_mcp.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_mcp_gui.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_measure.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_mining.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_ml.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_multiset.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_ocel.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_packages.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_parallel_and_loading.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_parallel_boxes.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_performance.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_regions.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_rename.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_report.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_researcher_api.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_simulation.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_snapshots.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_state_space.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_studio_gui.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_studio_lazy.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_teaching.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_tidy.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_updates.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_wf_structure.py +0 -0
- {openprocess-0.16.1 → openprocess-0.16.2}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprocess
|
|
3
|
-
Version: 0.16.
|
|
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:
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -316,6 +316,10 @@ class CPNet:
|
|
|
316
316
|
#: declarations. Recreated by :meth:`compile`.
|
|
317
317
|
self.evaluator = Evaluator(seed=seed)
|
|
318
318
|
self.errors: list[CompileIssue] = []
|
|
319
|
+
#: What the model says that OpenProcess does not do (a transition's
|
|
320
|
+
#: priority, its code segment): it still simulates, differently from
|
|
321
|
+
#: CPN Tools. Filled by :meth:`compile`.
|
|
322
|
+
self.warnings: list[CompileIssue] = []
|
|
319
323
|
#: place id -> the id under which its tokens are stored in a Marking.
|
|
320
324
|
#: Identity for ordinary places; the group representative for places in
|
|
321
325
|
#: a fusion set (see :meth:`marking_key`).
|
|
@@ -360,6 +364,7 @@ class CPNet:
|
|
|
360
364
|
empty list means the model is ready to simulate.
|
|
361
365
|
"""
|
|
362
366
|
self.errors = []
|
|
367
|
+
self.warnings = []
|
|
363
368
|
self.evaluator = Evaluator(seed=self.seed)
|
|
364
369
|
self._build_fusion_map()
|
|
365
370
|
|
|
@@ -396,6 +401,20 @@ class CPNet:
|
|
|
396
401
|
transition.guard_ast = parse_guard(guard)
|
|
397
402
|
except CPNMLError as error:
|
|
398
403
|
self._record(transition.id, transition.name, "guard", str(error))
|
|
404
|
+
priority = transition.priority_text.strip()
|
|
405
|
+
if priority and priority != "P_NORMAL":
|
|
406
|
+
self.warnings.append(CompileIssue(
|
|
407
|
+
transition.id, transition.name, "priority",
|
|
408
|
+
f"the priority {priority} is not used: priorities are not implemented, so "
|
|
409
|
+
"this transition can fire whenever it is enabled, also when CPN Tools "
|
|
410
|
+
"would let another one go first. Simulation and the state space can "
|
|
411
|
+
"differ from CPN Tools."))
|
|
412
|
+
if transition.code_text.strip():
|
|
413
|
+
self.warnings.append(CompileIssue(
|
|
414
|
+
transition.id, transition.name, "code segment",
|
|
415
|
+
"the code segment is not run: code segments are not implemented, so its "
|
|
416
|
+
"action does not happen and the variables it would set stay unbound or "
|
|
417
|
+
"are bound freely. Simulation and the state space can differ from CPN Tools."))
|
|
399
418
|
delay = transition.time_text.strip().lstrip("@+").strip()
|
|
400
419
|
if delay:
|
|
401
420
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprocess
|
|
3
|
-
Version: 0.16.
|
|
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
|