cpncheck 0.2.0__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.
- cpncheck-0.2.0/LICENSE +21 -0
- cpncheck-0.2.0/PKG-INFO +556 -0
- cpncheck-0.2.0/README.md +521 -0
- cpncheck-0.2.0/cpncheck/__init__.py +62 -0
- cpncheck-0.2.0/cpncheck/askctl.py +352 -0
- cpncheck-0.2.0/cpncheck/askctl_input.py +1120 -0
- cpncheck-0.2.0/cpncheck/checker.py +992 -0
- cpncheck-0.2.0/cpncheck/cli.py +325 -0
- cpncheck-0.2.0/cpncheck/coloured.py +1633 -0
- cpncheck-0.2.0/cpncheck/colourset.py +1000 -0
- cpncheck-0.2.0/cpncheck/combined.py +505 -0
- cpncheck-0.2.0/cpncheck/conformance.py +132 -0
- cpncheck-0.2.0/cpncheck/counterexample.py +333 -0
- cpncheck-0.2.0/cpncheck/cpnml.py +2249 -0
- cpncheck-0.2.0/cpncheck/ctl.py +210 -0
- cpncheck-0.2.0/cpncheck/equivalence.py +455 -0
- cpncheck-0.2.0/cpncheck/formula.py +1646 -0
- cpncheck-0.2.0/cpncheck/mllib.py +953 -0
- cpncheck-0.2.0/cpncheck/mutation.py +329 -0
- cpncheck-0.2.0/cpncheck/net.py +212 -0
- cpncheck-0.2.0/cpncheck/oracle.py +643 -0
- cpncheck-0.2.0/cpncheck/profile.py +350 -0
- cpncheck-0.2.0/cpncheck/profiles/__init__.py +12 -0
- cpncheck-0.2.0/cpncheck/profiles/statespace_report.py +439 -0
- cpncheck-0.2.0/cpncheck/properties.py +97 -0
- cpncheck-0.2.0/cpncheck/query.py +600 -0
- cpncheck-0.2.0/cpncheck/reduction.py +1237 -0
- cpncheck-0.2.0/cpncheck/result.py +466 -0
- cpncheck-0.2.0/cpncheck/selectors.py +338 -0
- cpncheck-0.2.0/cpncheck/smlvalue.py +355 -0
- cpncheck-0.2.0/cpncheck/statespace.py +271 -0
- cpncheck-0.2.0/cpncheck/structural.py +268 -0
- cpncheck-0.2.0/cpncheck/sweepline.py +483 -0
- cpncheck-0.2.0/cpncheck/symmetry.py +398 -0
- cpncheck-0.2.0/cpncheck.egg-info/PKG-INFO +556 -0
- cpncheck-0.2.0/cpncheck.egg-info/SOURCES.txt +61 -0
- cpncheck-0.2.0/cpncheck.egg-info/dependency_links.txt +1 -0
- cpncheck-0.2.0/cpncheck.egg-info/entry_points.txt +2 -0
- cpncheck-0.2.0/cpncheck.egg-info/requires.txt +6 -0
- cpncheck-0.2.0/cpncheck.egg-info/top_level.txt +1 -0
- cpncheck-0.2.0/pyproject.toml +50 -0
- cpncheck-0.2.0/setup.cfg +4 -0
- cpncheck-0.2.0/tests/test_askctl_input.py +165 -0
- cpncheck-0.2.0/tests/test_binding.py +150 -0
- cpncheck-0.2.0/tests/test_checker.py +248 -0
- cpncheck-0.2.0/tests/test_combined.py +212 -0
- cpncheck-0.2.0/tests/test_corpus.py +51 -0
- cpncheck-0.2.0/tests/test_cpnml.py +264 -0
- cpncheck-0.2.0/tests/test_cpnml_nets.py +197 -0
- cpncheck-0.2.0/tests/test_cpntools_samples.py +118 -0
- cpncheck-0.2.0/tests/test_declarations.py +139 -0
- cpncheck-0.2.0/tests/test_equivalence.py +249 -0
- cpncheck-0.2.0/tests/test_examples.py +55 -0
- cpncheck-0.2.0/tests/test_feature_nets.py +122 -0
- cpncheck-0.2.0/tests/test_generic.py +234 -0
- cpncheck-0.2.0/tests/test_mllib.py +63 -0
- cpncheck-0.2.0/tests/test_net_reading.py +279 -0
- cpncheck-0.2.0/tests/test_query.py +226 -0
- cpncheck-0.2.0/tests/test_reduction.py +156 -0
- cpncheck-0.2.0/tests/test_statespace_report.py +199 -0
- cpncheck-0.2.0/tests/test_structural.py +111 -0
- cpncheck-0.2.0/tests/test_sweepline.py +174 -0
- cpncheck-0.2.0/tests/test_timed.py +246 -0
cpncheck-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GrupoCybersegurancaVirtus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cpncheck-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cpncheck
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Model checking for Coloured Petri Nets, cross-checked against CPN Tools
|
|
5
|
+
Author: GrupoCybersegurancaVirtus
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/GrupoCybersegurancaVirtus/cpncheck
|
|
8
|
+
Project-URL: Documentation, https://grupocybersegurancavirtus.github.io/cpncheck/
|
|
9
|
+
Project-URL: Changelog, https://github.com/GrupoCybersegurancaVirtus/cpncheck/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/GrupoCybersegurancaVirtus/cpncheck/issues
|
|
11
|
+
Keywords: petri nets,coloured petri nets,CPN Tools,model checking,CTL,state space,formal verification
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering
|
|
26
|
+
Classifier: Topic :: Software Development :: Testing
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest; extra == "test"
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: pdoc; extra == "docs"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# CPNCheck
|
|
37
|
+
|
|
38
|
+
Model checking for Coloured Petri Nets, from Python, on the `.cpn` file CPN
|
|
39
|
+
Tools itself would open — and cross-checked against CPN Tools.
|
|
40
|
+
|
|
41
|
+
CPN Tools answers verification questions interactively: enter the state-space
|
|
42
|
+
tool, calculate the occurrence graph and the SCC graph, paste an ASK-CTL script
|
|
43
|
+
into an auxiliary box, evaluate it, read the answer. CPNCheck answers the same
|
|
44
|
+
questions from a script, so verification can run on every build of every model,
|
|
45
|
+
and then it can run CPN Tools headlessly and check that the two engines agree.
|
|
46
|
+
|
|
47
|
+
Zero dependencies: the Python standard library is enough.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install cpncheck # from PyPI
|
|
53
|
+
pip install git+https://github.com/GrupoCybersegurancaVirtus/cpncheck # latest
|
|
54
|
+
pip install -e ".[test]" # a checkout
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Python 3.8 or later, on Linux, macOS or Windows. CPN Tools is *not* needed;
|
|
58
|
+
it is used only by the optional cross-check (`cpncheck oracle`, Windows).
|
|
59
|
+
The API reference is at
|
|
60
|
+
<https://grupocybersegurancavirtus.github.io/cpncheck/>; the
|
|
61
|
+
[examples](examples/) are a guided tour.
|
|
62
|
+
|
|
63
|
+
## Use
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cpncheck check model.cpn # the state-space report
|
|
67
|
+
cpncheck check model.cpn --props my.props # plus your own properties
|
|
68
|
+
cpncheck check model.cpn --json verdicts.json --latex
|
|
69
|
+
cpncheck askctl model.cpn -o askctl.sml # the same questions, for CPN Tools
|
|
70
|
+
cpncheck mutants model.cpn mutants/ # one mutant per property (profiles
|
|
71
|
+
# that supply mutation operators)
|
|
72
|
+
cpncheck oracle model.cpn # run CPN Tools and compare
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from cpncheck import check_cpn
|
|
77
|
+
|
|
78
|
+
result = check_cpn("model.cpn", properties="AG !(has(Eat, ph(1)) & has(Eat, ph(2)))")
|
|
79
|
+
print(result.report())
|
|
80
|
+
assert result.passed
|
|
81
|
+
for r in result.failures:
|
|
82
|
+
print(r.id, r.detail) # the counterexample, rendered
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Budget.** Up to 1 000 000 markings are explored by default
|
|
86
|
+
(`--max-nodes`, `max_nodes=`); beyond it the state-space properties get no
|
|
87
|
+
verdict rather than a guess. That much costs memory: the Coloured Petri Nets
|
|
88
|
+
book's queue protocol, cut off at a million markings (5.6 million arcs),
|
|
89
|
+
took 279 s and 3.9 GiB. Lower the budget on a small machine, or use a
|
|
90
|
+
reduction.
|
|
91
|
+
|
|
92
|
+
`ModelCheckResult` exposes `passed`, `failures`, `skipped`, `properties`,
|
|
93
|
+
`stats`, `oracle`, `report_data`, `report()`, `to_dict()`, `latex_rows()` and
|
|
94
|
+
`latex_table()`.
|
|
95
|
+
|
|
96
|
+
**CPNCheck does not need CPN Tools.** It is pure Python with no dependencies:
|
|
97
|
+
reading the `.cpn`, the CPN ML, the occurrence graph, CTL, reduction and
|
|
98
|
+
counterexamples are all its own. CPN Tools is only used by `cpncheck oracle`,
|
|
99
|
+
the optional cross-check that proves the answers right; those tests skip
|
|
100
|
+
themselves when it is not installed, and a test runs the checker with the
|
|
101
|
+
oracle module blocked so that this cannot change by accident.
|
|
102
|
+
|
|
103
|
+
### Any net: the state-space report
|
|
104
|
+
|
|
105
|
+
A net that no specific profile recognises gets the default one, which reports
|
|
106
|
+
what CPN Tools' state-space report does:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cpncheck check SimpleProtocol.cpn
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| | | |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| R1 | Deadlock freedom | `AG !dead` |
|
|
115
|
+
| R2 | Termination | `AF dead` |
|
|
116
|
+
| R3 | Home marking | one terminal SCC |
|
|
117
|
+
| R4 | No dead transitions | `forall t: EF EN(t)` |
|
|
118
|
+
| R5 | All transitions live | `forall t: AG EF EN(t)` |
|
|
119
|
+
|
|
120
|
+
They are facts, printed YES/NO, and never fail a check -- a protocol meant to
|
|
121
|
+
stop has dead markings and is not wrong for it. Below them come the integer
|
|
122
|
+
and multiset bounds of every place, the dead and live transition instances and
|
|
123
|
+
the fairness of each transition (impartial, fair, just). Every figure is
|
|
124
|
+
compared with CPN Tools' own functions (`ListDeadTIs`, `ListLiveTIs`,
|
|
125
|
+
`UpperInteger`, `LowerInteger`, `UpperMultiSet`, `LowerMultiSet`,
|
|
126
|
+
`ListImpartialTIs`, `ListFairTIs`, `ListJustTIs`) by the oracle tests.
|
|
127
|
+
|
|
128
|
+
### Your own properties, without Python
|
|
129
|
+
|
|
130
|
+
Requirements go in a file (or on the command line) and fail the check when
|
|
131
|
+
violated, with a counterexample:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
# dining.props
|
|
135
|
+
ME "neighbours never eat together": AG !(has(Eat, ph(1)) & has(Eat, ph(2)))
|
|
136
|
+
NOEAT "philosopher 1 never eats": AG !has(Eat, ph(1))
|
|
137
|
+
CS: AG tokens("Unused Chopsticks") <= 5
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cpncheck check DiningPhilosophers.cpn --props dining.props --prop "AF has(Eat, ph(3))"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Propositions: `dead`, `enabled(T)`, `marked(P)`, `empty(P)`,
|
|
145
|
+
`tokens(P) op k`, `enabled_count(T) op k`, `has(P, v)` with `v` any CPN ML
|
|
146
|
+
value; connectives `! & | ->`; operators `AG AF EF EG`, `E[f U g]`,
|
|
147
|
+
`A[f U g]`. Names are bare or quoted and refer to a full path, a page's
|
|
148
|
+
element or every element of that name. The full grammar is in
|
|
149
|
+
[`cpncheck/query.py`](cpncheck/query.py); from Python, pass
|
|
150
|
+
`properties=` to `check_cpn` or `CPNModelChecker`. Every user property on two
|
|
151
|
+
CPN Tools samples is checked against CPN Tools' own ASK-CTL by the tests.
|
|
152
|
+
|
|
153
|
+
### ASK-CTL queries you already have
|
|
154
|
+
|
|
155
|
+
A file of ASK-CTL queries, as one would evaluate it in CPN Tools after
|
|
156
|
+
*Enter State Space*, is accepted as it is:
|
|
157
|
+
|
|
158
|
+
```sml
|
|
159
|
+
fun Eating n = Mark.Top'Eat 1 n <> empty;
|
|
160
|
+
val AlwaysAgain = eval_node (INV (POS (NF ("eating", Eating)))) InitNode;
|
|
161
|
+
eval_node (POS (EXIST_MODAL (AF ("put", fn a => ArcToTI a = TI.Top'Put_Down_Chopsticks 1), TT))) InitNode;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cpncheck check DiningPhilosophers.cpn --askctl dining.sml
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Every top-level `eval_node F InitNode` is a property (named after its `val`,
|
|
169
|
+
or `Q1`, `Q2`, ...). The rest of the file is CPN ML, with the net's
|
|
170
|
+
declarations and CPN Tools' state-space functions (`Mark`, `OutArcs`,
|
|
171
|
+
`ArcToBE`, `Bind`, `TI`, `SearchNodes`, `PredAllNodes`, `ListDeadMarkings`,
|
|
172
|
+
`NodesInPath`, the SCC functions, ...). The formulas mean what
|
|
173
|
+
`ASKCTL.sml` makes them mean, which is not textbook CTL: `EV` holds vacuously
|
|
174
|
+
at a dead marking, and `POS (INV a)`, `POS (ALONG a)` and `EV (INV a)` are
|
|
175
|
+
decided on the SCC graph, where it does not. The tests hold 49 queries
|
|
176
|
+
written to tell those readings apart, and all of them get CPN Tools' verdict
|
|
177
|
+
([`cpncheck/askctl_input.py`](cpncheck/askctl_input.py)). From Python:
|
|
178
|
+
`check_cpn(net, askctl="dining.sml")`.
|
|
179
|
+
|
|
180
|
+
**Examples.** [examples/](examples/) is a guided tour, one feature per
|
|
181
|
+
script, on small nets shipped with them: the quickstart, the layers under
|
|
182
|
+
`check_cpn`, the property language, counterexamples, timed nets, the four
|
|
183
|
+
reductions, writing a profile with mutation operators and a conformance
|
|
184
|
+
test, ASK-CTL and report formats, the CPN Tools oracle and the command line.
|
|
185
|
+
`python examples/run_all.py` runs them all.
|
|
186
|
+
|
|
187
|
+
## Counterexamples
|
|
188
|
+
|
|
189
|
+
A violated property hands back evidence, not a sentence. `r.cex` is a
|
|
190
|
+
`Counterexample`: the occurrence sequence, the binding of every step, the
|
|
191
|
+
marking it ends in, and the input the net was given.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
cex = result.properties["A3"].cex
|
|
195
|
+
cex.kind # finite | lasso | witness | structural | conformance
|
|
196
|
+
cex.sequence(bindings=True) # Distribute<x = (2, 7)> -> Init<...> -> ...
|
|
197
|
+
cex.input # the input this violation needs
|
|
198
|
+
cex.replay(checker.cnet) # True: it really does what it says
|
|
199
|
+
cex.to_dict() # the whole trace, for a report or a test
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`cpncheck check model.cpn --trace` prints all of it.
|
|
203
|
+
|
|
204
|
+
Three things worth knowing:
|
|
205
|
+
|
|
206
|
+
**A liveness property can fail with no finite witness at all.** If the net can
|
|
207
|
+
run forever without ever reaching the goal, the evidence has to include the
|
|
208
|
+
repeating part. That is the `lasso` shape: a prefix, then a cycle the net can
|
|
209
|
+
take indefinitely — `... -> Feed_1 then forever [Rule2]`.
|
|
210
|
+
|
|
211
|
+
**`replay()` is a certificate.** It re-fires the sequence using the net's
|
|
212
|
+
firing rule, not the search that produced it, and checks it ends where the
|
|
213
|
+
counterexample claims (and, for a lasso, returns to where the cycle began). A
|
|
214
|
+
bug in the fixpoints cannot forge a trace that replays. The test suite requires
|
|
215
|
+
every sequence from every mutant to replay, and requires a tampered one not to.
|
|
216
|
+
|
|
217
|
+
**A structural finding comes with an input.** A profile's structural check
|
|
218
|
+
(guard disjointness, for the tree profile) holds or fails for *every* input,
|
|
219
|
+
so a failure names no particular run — but it hands back a point on which the
|
|
220
|
+
problem really happens, as the counterexample's `input`, to be checked with
|
|
221
|
+
`samples=[...]`.
|
|
222
|
+
|
|
223
|
+
## What it does
|
|
224
|
+
|
|
225
|
+
Four techniques, kept apart because they establish different things:
|
|
226
|
+
|
|
227
|
+
| Technique | What a verdict means |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `ctl` | Explicit-state CTL on the occurrence graph. Real token values, no abstraction. True *for the initial marking it was given*. |
|
|
230
|
+
| `scc` | Home markings from the SCC decomposition — what CPN Tools' `ListHomeMarkings` computes. |
|
|
231
|
+
| `structural` | Static analysis of guards that are conjunctions of bounds. Holds for **every** input, which no single state space can show. |
|
|
232
|
+
| `testing` | Conformance with a reference function on a set of inputs. Not a temporal property. |
|
|
233
|
+
|
|
234
|
+
Dead markings are given a self-loop, so a maximal path that ends before φ is a
|
|
235
|
+
counterexample to `AF φ`. CPN Tools' ASK-CTL reads `EV` as vacuously true at a
|
|
236
|
+
dead marking; both readings are computed, and the generated ASK-CTL encodes
|
|
237
|
+
`AF φ` as `EV(φ) AND NOT E[¬φ U (dead ∧ ¬φ)]` so CPN Tools answers the textbook
|
|
238
|
+
question.
|
|
239
|
+
|
|
240
|
+
## State explosion: partial-order reduction
|
|
241
|
+
|
|
242
|
+
A net made of components that do not interact until the end has an occurrence
|
|
243
|
+
graph that interleaves them in every order. A random forest of `n` trees has
|
|
244
|
+
exactly `2^n + 2` markings, although every order leads to the same vote.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
cpncheck check model.cpn --reduction stubborn
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
result = check_cpn("model.cpn", properties="AF dead", reduction="stubborn")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
With `reduction="stubborn"` every property is decided on a *stubborn-set*
|
|
255
|
+
reduced graph (`cpncheck/reduction.py`) that provably gives the full graph's
|
|
256
|
+
verdict. Which reduced graph depends on what the property needs:
|
|
257
|
+
|
|
258
|
+
| Graph | Conditions | Preserves | Used for |
|
|
259
|
+
|---|---|---|---|
|
|
260
|
+
| `deadlock` | D0–D2 (Valmari) | every dead marking; existence of an infinite run | `AF dead`, `AG(dead → ψ)`, home markings of a terminating net |
|
|
261
|
+
| `safety` | D0–D2 + an up-set for `¬ψ` (Kristensen & Valmari 2000) | reachability of a marking violating `ψ` | `AG ψ`, `!EF χ`, `EF χ` with `ψ`, `χ` propositional |
|
|
262
|
+
| `stutter` | D0–D2 + C2 (one invisible element or all), C3 (cycle proviso), C4 (Gerth et al. 1999) | every CTL-X formula | `AF`, `EF`, `EU`, `AG EF` |
|
|
263
|
+
| `full` | none | everything | whatever the theorems do not cover |
|
|
264
|
+
|
|
265
|
+
A property is first split — `forall` into its instances, `AG` of a conjunction
|
|
266
|
+
or of a bound on many places into one `AG` per conjunct or per place — because
|
|
267
|
+
a part that observes fewer places reduces more. The report says, per property,
|
|
268
|
+
which graph decided it.
|
|
269
|
+
|
|
270
|
+
On the tree nets the deadlock graph has `n + 3` markings for `n` trees (a
|
|
271
|
+
forest of 100 trees: 103 instead of about 1.3 × 10^30). One property does not
|
|
272
|
+
reduce: `AF EN(Vote)` observes the output place of every tree, so every leaf is
|
|
273
|
+
visible and none can be reordered — for large forests it is reported without a
|
|
274
|
+
verdict, and the report says why. The reduction is checked the hard way: on
|
|
275
|
+
both fixture nets and all their mutants, every verdict equals the full graph's
|
|
276
|
+
and every reduced counterexample replays (`tests/test_reduction.py`). CPN Tools
|
|
277
|
+
has no such reduction, so `oracle` values are only produced without it.
|
|
278
|
+
|
|
279
|
+
## State explosion: the sweep-line method
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
cpncheck check model.cpn --reduction sweep --progress "tokens(Done) - tokens(Todo)"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
result = check_cpn("model.cpn", properties="AG tokens(Buffer) <= 3",
|
|
287
|
+
reduction="sweep", progress="tokens(Done) - tokens(Todo)")
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The sweep-line method (`cpncheck/sweepline.py`) explores the markings in the
|
|
291
|
+
order of a *progress measure* and forgets the ones it has left behind, so it
|
|
292
|
+
needs memory for a moving window of the graph, not for all of it. The
|
|
293
|
+
generalised form is used (Kristensen & Mailund 2002): an arc to a marking with
|
|
294
|
+
less progress makes it *persistent* and starts a new sweep from it, so every
|
|
295
|
+
reachable marking is explored with **any** measure. A bad measure costs time
|
|
296
|
+
(markings explored again), never a wrong verdict.
|
|
297
|
+
|
|
298
|
+
It decides properties of single markings: `AG ψ`, `EF χ` and `!EF χ` for
|
|
299
|
+
state predicates, which may mention `dead` and `enabled`. Path properties
|
|
300
|
+
(`AF`, `EG`, `AG EF`, home markings) get no verdict, with the reason, because
|
|
301
|
+
the arcs are not kept; a violation is reported as the marking, not a path.
|
|
302
|
+
|
|
303
|
+
A measure is a sum of `[k *] tokens(P)`, `[k *] sum(P)` (the integer values of
|
|
304
|
+
the tokens), `[k *] clock` and constants, or a Python function of the marking.
|
|
305
|
+
A timed net needs none: its clock never goes back. On a finite variant of CPN
|
|
306
|
+
Tools' TimedResourceAlloc, 16 114 markings are explored with at most 2 293
|
|
307
|
+
stored, in one sweep. `tests/test_sweepline.py` compares every verdict with
|
|
308
|
+
the full graph on both fixture nets and all their mutants, under no measure, a
|
|
309
|
+
token count and an arbitrary non-monotone function.
|
|
310
|
+
|
|
311
|
+
## State explosion: equivalence and symmetry
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
cpncheck check timed.cpn --reduction equivalence # time equivalence
|
|
315
|
+
cpncheck check dining.cpn --reduction symmetry --symmetry PH,CS=rotation
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
check_cpn("dining.cpn", reduction="symmetry", symmetry={"PH,CS": "rotation"})
|
|
320
|
+
check_cpn("model.cpn", reduction="equivalence", equivalence=my_canonical_form)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Both explore one representative per class of equivalent markings — the
|
|
324
|
+
*quotient* graph (`cpncheck/equivalence.py`, `cpncheck/symmetry.py`). It
|
|
325
|
+
gives the full graph's CTL verdicts when the equivalence is *consistent*
|
|
326
|
+
(equivalent markings reach the same classes) and the property cannot tell
|
|
327
|
+
equivalent markings apart.
|
|
328
|
+
|
|
329
|
+
* **Time equivalence** (timed nets): markings equal up to a shift of time are
|
|
330
|
+
one class. Exact unless an inscription reads `time()`, in which case it is
|
|
331
|
+
refused by name. A net whose graph is infinite only because time goes on
|
|
332
|
+
gets a finite quotient; on the TimedResourceAlloc variant, 16 114 markings
|
|
333
|
+
become 970 classes.
|
|
334
|
+
* **Symmetry**: the colour sets whose values may be permuted, all at once or
|
|
335
|
+
by rotation, several together by position (`PH,CS=rotation`). The
|
|
336
|
+
conditions are checked, not assumed: the initial marking must be invariant
|
|
337
|
+
under each generator, and every explored representative is tested — the
|
|
338
|
+
successors of `g(M)` must be the images of those of `M`. A guard that breaks
|
|
339
|
+
the symmetry fails the test and the verdicts are withheld; a property that
|
|
340
|
+
singles out a permuted value (`has(Eat, ph(1))`) gets no verdict. Dining
|
|
341
|
+
philosophers: 11 markings, 3 classes; DistributedDataBase: 109 markings, 11
|
|
342
|
+
classes.
|
|
343
|
+
* **Your own canonical form**: consistency is tested on sampled markings,
|
|
344
|
+
not proved, and the verdicts are reported as conditional on the test.
|
|
345
|
+
|
|
346
|
+
Not preserved by any of them: the home-marking property (a class can be a
|
|
347
|
+
home class while none of its markings is a home marking) and the graph
|
|
348
|
+
statistics. `tests/test_equivalence.py` compares every verdict with the full
|
|
349
|
+
graph, and checks that a symmetry the net does not have, an equivalence that
|
|
350
|
+
merges different behaviours, and a property naming a permuted value all end
|
|
351
|
+
without a verdict rather than with a wrong one.
|
|
352
|
+
|
|
353
|
+
## State explosion: combining reductions
|
|
354
|
+
|
|
355
|
+
The reductions act on different things -- which successors are explored,
|
|
356
|
+
which markings are kept, which markings are told apart -- so they compose:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
cpncheck check Workers.cpn --reduction stubborn+symmetry --symmetry WORKER=all
|
|
360
|
+
cpncheck check Pipelines.cpn --reduction sweep+stubborn --progress 'tokens("Done 1")'
|
|
361
|
+
cpncheck check Loop.cpn --reduction equivalence+symmetry --symmetry TOKEN=all
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
| Combination | What it decides |
|
|
365
|
+
|---|---|
|
|
366
|
+
| `equivalence+symmetry` | One quotient by the equivalence both generate: symmetry representative of the time representative. Time and symmetry commute, so the composition is a canonical form. Everything a quotient decides. |
|
|
367
|
+
| `stubborn` + a quotient | `deadlock` and `safety` stubborn graphs explored on class representatives; other properties on the plain quotient. |
|
|
368
|
+
| `sweep` + a quotient | The sweep over representatives: `AG psi`, `EF chi`, `!EF chi` the equivalence does not observe. |
|
|
369
|
+
| `sweep+stubborn` (+ a quotient) | The sweep over the `deadlock` or `safety` stubborn graph of each part: a stubborn choice depends on the marking alone, so the sweep reaches every marking of the reduced graph, dead ones and invariant violations included. |
|
|
370
|
+
|
|
371
|
+
The `stutter` stubborn graph is not combined: its cycle proviso is stated on
|
|
372
|
+
the search stack, which neither a quotient nor the sweep keeps. Where
|
|
373
|
+
stubborn sets do not apply (a global clock, priorities, inhibitor or reset
|
|
374
|
+
arcs, source transitions) they are dropped with a warning and the rest of the
|
|
375
|
+
combination still runs. Nothing combined decides the home-marking property.
|
|
376
|
+
|
|
377
|
+
| `AG !dead` on | full | alone | combined |
|
|
378
|
+
|---|---|---|---|
|
|
379
|
+
| 6 identical workers | 729 | stubborn 127, symmetry 28 | `stubborn+symmetry`: 13 |
|
|
380
|
+
| 6 identical workers | 729 | sweep stores 433 at once | `sweep+stubborn+symmetry`: 8 stored at once |
|
|
381
|
+
| 8 independent pipelines | 6 561 | stubborn 17 | `sweep+stubborn`: 16 stored at once |
|
|
382
|
+
| symmetric timed loop | infinite | time 7 classes | `equivalence+symmetry`: 5 |
|
|
383
|
+
|
|
384
|
+
`tests/test_combined.py` compares every combination with the full graph on
|
|
385
|
+
properties of every kind, and checks that each one decides what it claims
|
|
386
|
+
to; on the corpus nets, `sweep+stubborn` with a deliberately bad progress
|
|
387
|
+
measure (`hash(m) % 7`, regress arcs everywhere) agrees with the full graph
|
|
388
|
+
on every net. The composed consistency test still refuses a net that is not
|
|
389
|
+
symmetric (`timed_loop`, whose delay depends on the token).
|
|
390
|
+
|
|
391
|
+
## Two claims, and how they are held up
|
|
392
|
+
|
|
393
|
+
**Sensitivity.** `cpncheck mutants` derives one mutant per property — a small
|
|
394
|
+
edit of a guard, an inscription or an arc that makes exactly that property
|
|
395
|
+
false. Agreement on correct nets alone would also be reached by two checkers
|
|
396
|
+
that always answer "true". The test suite runs every mutant and requires the
|
|
397
|
+
targeted property to fail.
|
|
398
|
+
|
|
399
|
+
**Agreement.** `cpncheck oracle` runs the CPN Tools 4.0.1 simulator headlessly
|
|
400
|
+
through Access/CPN, enters the state-space tool the way the GUI's *Enter State
|
|
401
|
+
Space* does, builds the occurrence and SCC graphs and evaluates the ASK-CTL
|
|
402
|
+
queries. Every statistic and every verdict is compared. Requires CPN Tools,
|
|
403
|
+
CPN IDE (for its bundled Access/CPN) and a 32-bit Java 8 with `jjs`;
|
|
404
|
+
`CPNToolsOracle().missing()` lists anything absent.
|
|
405
|
+
|
|
406
|
+
## Verifying your own nets
|
|
407
|
+
|
|
408
|
+
Properties are data, not code. A *profile* says which places and transitions
|
|
409
|
+
play which role in your nets and which properties they should satisfy; the
|
|
410
|
+
checker knows only markings, arcs and formulas.
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
import collections
|
|
414
|
+
from cpncheck import CPNModelChecker
|
|
415
|
+
from cpncheck.formula import AF, AG, Dead, MaxTokens
|
|
416
|
+
from cpncheck.profile import Profile
|
|
417
|
+
from cpncheck.selectors import Places
|
|
418
|
+
|
|
419
|
+
class MyProfile(Profile):
|
|
420
|
+
name = "my nets"
|
|
421
|
+
catalog = my_property_descriptors
|
|
422
|
+
|
|
423
|
+
def matches(self, net):
|
|
424
|
+
return any(p["name"] == "Done" for p in net.places)
|
|
425
|
+
|
|
426
|
+
def ctl_formulas(self, net, groups, labels):
|
|
427
|
+
return collections.OrderedDict([
|
|
428
|
+
("T1", AF(Dead().phrased("the end"))),
|
|
429
|
+
("T2", AG(MaxTokens(Places(name="Done"), 1))),
|
|
430
|
+
])
|
|
431
|
+
|
|
432
|
+
print(CPNModelChecker("mine.cpn", profile=MyProfile()).check().report())
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Available: `Dead`, `Marked`, `AllMarked`, `MaxTokens`, `TokensIn`, `Enabled`,
|
|
436
|
+
`EnabledCount`; `Not`, `And`, `Or`, `Implies`; `AG`, `AF`, `EF`, `EU`; and
|
|
437
|
+
`ForEach`, which quantifies over a group of pages and blames the one that
|
|
438
|
+
failed. Register a profile with `cpncheck.profile.register` and it is detected
|
|
439
|
+
automatically.
|
|
440
|
+
|
|
441
|
+
Each formula does two things beyond being true or false: **it explains itself**
|
|
442
|
+
when it fails (the shortest occurrence sequence to the violation, and why that
|
|
443
|
+
marking is bad), and **it renders to ASK-CTL**, so the Python check and the CPN
|
|
444
|
+
Tools script are the same question by construction.
|
|
445
|
+
|
|
446
|
+
The other three techniques are equally yours to use. A profile's
|
|
447
|
+
`static_checks` can call `cpncheck.structural.overlapping_guards` to ask
|
|
448
|
+
whether two branches of a choice are ever satisfiable together — an answer that
|
|
449
|
+
covers *every* input; `conformance` can call
|
|
450
|
+
`cpncheck.conformance.check_conformance` with any reference implementation; and
|
|
451
|
+
`mutation_operators` supplies the faults your properties must catch, built from
|
|
452
|
+
`cpncheck.mutation.CPNDocument`.
|
|
453
|
+
|
|
454
|
+
## What it understands of CPN ML
|
|
455
|
+
|
|
456
|
+
Guards and inscriptions are parsed, not pattern-matched: `if`/`then`/`else`,
|
|
457
|
+
`let`/`in`/`end`, `case`/`of`, `fn`, function application, the operators with
|
|
458
|
+
their SML precedences and associativities, lists, tuples, records (including
|
|
459
|
+
flexible patterns `{data, ...}`), multisets, characters, string escapes,
|
|
460
|
+
sequences `(e1; e2)`, `SOME`/`NONE`, operator sections `(op +)`, composition
|
|
461
|
+
`o`, references (`globref`, `!`) and type annotations. The expression compiles
|
|
462
|
+
to a closure, so firing stays fast.
|
|
463
|
+
|
|
464
|
+
**The standard library is CPN Tools' own** (`cpncheck/mllib.py`): the SML
|
|
465
|
+
Basis functions nets use (`Int`, `Real`, `Math`, `String`, `Char`, `Option`,
|
|
466
|
+
`List`, `ListPair`), CPN Tools' list functions (`mem`, `rm`, `contains`,
|
|
467
|
+
`intersect`, `listsub`, `ins_new`, `sort`...), its multiset functions (`cf`,
|
|
468
|
+
`size`, `ms_to_col`, `ext_col`, `filter`, `==`, `<<=`...) and, per colour
|
|
469
|
+
set, `size`, `ord`, `col`, `legal`, `mkstr`, `mkstr_ms`, `all`, `mult`. Each
|
|
470
|
+
name follows SML's calling convention (`Int.max (a, b)` takes a pair), and
|
|
471
|
+
each is checked against CPN Tools: `tests/data/mllib_cases.txt` was
|
|
472
|
+
evaluated by CPN Tools itself and `tests/test_mllib.py` requires the same
|
|
473
|
+
value -- or the same exception -- from CPNCheck. As in CPN Tools, a multiset
|
|
474
|
+
is also a list (`map f (CS.all ())`), and a list on an arc of a place that
|
|
475
|
+
does not hold lists is the multiset of its elements.
|
|
476
|
+
|
|
477
|
+
A net's own `val` and `fun` declarations are read and used, which means **a net
|
|
478
|
+
describes itself**. Nothing about what the tree nets compute is built into the
|
|
479
|
+
checker: their soft voting and `argmax` come from the `<ml>` blocks of the
|
|
480
|
+
`.cpn` file. Where a net leaves a function undeclared, supply it:
|
|
481
|
+
|
|
482
|
+
```python
|
|
483
|
+
check_cpn("model.cpn", prelude={"myFunction": lambda x: ...})
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Binding elements.** A variable that no input arc binds is enumerated over its
|
|
487
|
+
declared colour set, the way CPN Tools does it — `unit`, `bool`, enumerations,
|
|
488
|
+
index ranges and the products, records, subsets and aliases built from them. A
|
|
489
|
+
free variable whose colour set is infinite makes the net unexplorable and is
|
|
490
|
+
refused by name, never quietly ignored.
|
|
491
|
+
|
|
492
|
+
Also read: multisets anywhere in an expression, input arcs that are
|
|
493
|
+
expressions rather than patterns, enumeration constants, `index` colour sets,
|
|
494
|
+
`unit with e`, `bool with (no, yes)`, `union`, `subset ... by`/`with`, colour
|
|
495
|
+
sets written only as text, fusion sets, double arcs and several prime pages,
|
|
496
|
+
variables defined by a guard, transition priorities, transitions without
|
|
497
|
+
input arcs, code segments without side effects (`input`/`output`/`action`),
|
|
498
|
+
inhibitor and reset arcs, and timed nets with integer time (timestamps, `@+`
|
|
499
|
+
delays on arcs and transitions, `@+` on input arcs, `time()`, the
|
|
500
|
+
earliest-time rule). Where CPN Tools' documentation leaves a meaning open, it
|
|
501
|
+
was measured in CPN Tools (`tests/nets/features`). Not supported: synchronous
|
|
502
|
+
channels, code with side effects, real-valued time (`@++`) and random values
|
|
503
|
+
(`CS.ran()`, `discrete`, `exponential`) — the last two have no occurrence
|
|
504
|
+
graph. Anything outside the
|
|
505
|
+
fragment raises `UnsupportedNet` rather than being mis-read — a wrong reading
|
|
506
|
+
of a guard is a wrong verdict.
|
|
507
|
+
|
|
508
|
+
**Timed nets.** When a timed place holds several ready tokens of the value a
|
|
509
|
+
binding takes, each choice of token is its own successor. CPN Tools 4.0.1
|
|
510
|
+
explores a subset of those choices (not the same one on every run), so timed
|
|
511
|
+
nets are compared with it by containment: `oracle.compare_graph_containment`
|
|
512
|
+
checks that every node and arc it finds is in CPNCheck's graph. Stubborn-set
|
|
513
|
+
reduction is not sound with a global clock or with priorities; such nets are
|
|
514
|
+
always checked on the full graph.
|
|
515
|
+
|
|
516
|
+
How far this reaches is measured on the 24 example models CPN Tools ships
|
|
517
|
+
with: `python examples/cpntools_samples.py --oracle`. Today 16 are read and
|
|
518
|
+
explored, the 7 whose occurrence graph fits the budget agree with CPN Tools
|
|
519
|
+
on every statistic, and the 8 refused are all outside the target (side
|
|
520
|
+
effects, random values, real time). The plan to make CPNCheck check any net CPN Tools can
|
|
521
|
+
analyse is in [ROADMAP.md](ROADMAP.md).
|
|
522
|
+
|
|
523
|
+
## Profiles and plugins
|
|
524
|
+
|
|
525
|
+
What CPNCheck asks of a net comes from a *profile*. The built-in one, the
|
|
526
|
+
state-space report, applies to every net. A package can publish a profile for
|
|
527
|
+
its own kind of nets as a plugin, and CPNCheck picks it up when it is
|
|
528
|
+
installed:
|
|
529
|
+
|
|
530
|
+
```toml
|
|
531
|
+
[project.entry-points."cpncheck.profiles"]
|
|
532
|
+
my_nets = "my_package.cpn_profile:PROFILE"
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
A profile receives its settings as `options`, which CPNCheck hands on without
|
|
536
|
+
reading them -- `CPNModelChecker(path, options={...})`, or `-O key=value` on
|
|
537
|
+
the command line. A plugin that fails to load is reported as a warning and
|
|
538
|
+
skipped. pyruleanalyzer's profile for the nets it generates from decision
|
|
539
|
+
trees, random forests and gradient boosting is one such plugin: CPNCheck itself
|
|
540
|
+
knows nothing about trees, and its tests run with no plugin at all.
|
|
541
|
+
|
|
542
|
+
## Status
|
|
543
|
+
|
|
544
|
+
Pre-release, extracted from
|
|
545
|
+
[pyruleanalyzer](https://github.com/GrupoCybersegurancaVirtus/pyruleanalyzer),
|
|
546
|
+
which now uses it through the plugin above.
|
|
547
|
+
|
|
548
|
+
Agreement with CPN Tools is measured, not assumed: the CPN Tools samples, a
|
|
549
|
+
corpus of 48 nets other people wrote ([tests/corpus](tests/corpus/RESULTS.md)),
|
|
550
|
+
the constructs whose meaning was measured in CPN Tools (`tests/nets/features`)
|
|
551
|
+
and 236 expressions of the CPN ML library (`tests/data/mllib_cases.txt`) are
|
|
552
|
+
all compared with CPN Tools itself. [ROADMAP.md](ROADMAP.md) says what is
|
|
553
|
+
supported and what remains; [docs/history](docs/history/extraction-from-pyruleanalyzer.md)
|
|
554
|
+
how CPNCheck came out of pyruleanalyzer.
|
|
555
|
+
|
|
556
|
+
MIT licensed.
|