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.
Files changed (63) hide show
  1. cpncheck-0.2.0/LICENSE +21 -0
  2. cpncheck-0.2.0/PKG-INFO +556 -0
  3. cpncheck-0.2.0/README.md +521 -0
  4. cpncheck-0.2.0/cpncheck/__init__.py +62 -0
  5. cpncheck-0.2.0/cpncheck/askctl.py +352 -0
  6. cpncheck-0.2.0/cpncheck/askctl_input.py +1120 -0
  7. cpncheck-0.2.0/cpncheck/checker.py +992 -0
  8. cpncheck-0.2.0/cpncheck/cli.py +325 -0
  9. cpncheck-0.2.0/cpncheck/coloured.py +1633 -0
  10. cpncheck-0.2.0/cpncheck/colourset.py +1000 -0
  11. cpncheck-0.2.0/cpncheck/combined.py +505 -0
  12. cpncheck-0.2.0/cpncheck/conformance.py +132 -0
  13. cpncheck-0.2.0/cpncheck/counterexample.py +333 -0
  14. cpncheck-0.2.0/cpncheck/cpnml.py +2249 -0
  15. cpncheck-0.2.0/cpncheck/ctl.py +210 -0
  16. cpncheck-0.2.0/cpncheck/equivalence.py +455 -0
  17. cpncheck-0.2.0/cpncheck/formula.py +1646 -0
  18. cpncheck-0.2.0/cpncheck/mllib.py +953 -0
  19. cpncheck-0.2.0/cpncheck/mutation.py +329 -0
  20. cpncheck-0.2.0/cpncheck/net.py +212 -0
  21. cpncheck-0.2.0/cpncheck/oracle.py +643 -0
  22. cpncheck-0.2.0/cpncheck/profile.py +350 -0
  23. cpncheck-0.2.0/cpncheck/profiles/__init__.py +12 -0
  24. cpncheck-0.2.0/cpncheck/profiles/statespace_report.py +439 -0
  25. cpncheck-0.2.0/cpncheck/properties.py +97 -0
  26. cpncheck-0.2.0/cpncheck/query.py +600 -0
  27. cpncheck-0.2.0/cpncheck/reduction.py +1237 -0
  28. cpncheck-0.2.0/cpncheck/result.py +466 -0
  29. cpncheck-0.2.0/cpncheck/selectors.py +338 -0
  30. cpncheck-0.2.0/cpncheck/smlvalue.py +355 -0
  31. cpncheck-0.2.0/cpncheck/statespace.py +271 -0
  32. cpncheck-0.2.0/cpncheck/structural.py +268 -0
  33. cpncheck-0.2.0/cpncheck/sweepline.py +483 -0
  34. cpncheck-0.2.0/cpncheck/symmetry.py +398 -0
  35. cpncheck-0.2.0/cpncheck.egg-info/PKG-INFO +556 -0
  36. cpncheck-0.2.0/cpncheck.egg-info/SOURCES.txt +61 -0
  37. cpncheck-0.2.0/cpncheck.egg-info/dependency_links.txt +1 -0
  38. cpncheck-0.2.0/cpncheck.egg-info/entry_points.txt +2 -0
  39. cpncheck-0.2.0/cpncheck.egg-info/requires.txt +6 -0
  40. cpncheck-0.2.0/cpncheck.egg-info/top_level.txt +1 -0
  41. cpncheck-0.2.0/pyproject.toml +50 -0
  42. cpncheck-0.2.0/setup.cfg +4 -0
  43. cpncheck-0.2.0/tests/test_askctl_input.py +165 -0
  44. cpncheck-0.2.0/tests/test_binding.py +150 -0
  45. cpncheck-0.2.0/tests/test_checker.py +248 -0
  46. cpncheck-0.2.0/tests/test_combined.py +212 -0
  47. cpncheck-0.2.0/tests/test_corpus.py +51 -0
  48. cpncheck-0.2.0/tests/test_cpnml.py +264 -0
  49. cpncheck-0.2.0/tests/test_cpnml_nets.py +197 -0
  50. cpncheck-0.2.0/tests/test_cpntools_samples.py +118 -0
  51. cpncheck-0.2.0/tests/test_declarations.py +139 -0
  52. cpncheck-0.2.0/tests/test_equivalence.py +249 -0
  53. cpncheck-0.2.0/tests/test_examples.py +55 -0
  54. cpncheck-0.2.0/tests/test_feature_nets.py +122 -0
  55. cpncheck-0.2.0/tests/test_generic.py +234 -0
  56. cpncheck-0.2.0/tests/test_mllib.py +63 -0
  57. cpncheck-0.2.0/tests/test_net_reading.py +279 -0
  58. cpncheck-0.2.0/tests/test_query.py +226 -0
  59. cpncheck-0.2.0/tests/test_reduction.py +156 -0
  60. cpncheck-0.2.0/tests/test_statespace_report.py +199 -0
  61. cpncheck-0.2.0/tests/test_structural.py +111 -0
  62. cpncheck-0.2.0/tests/test_sweepline.py +174 -0
  63. 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.
@@ -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.