cpncheck 0.2.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,62 @@
1
+ """CPNCheck -- a model checker for Coloured Petri Nets.
2
+
3
+ CPN Tools answers verification questions interactively: enter the state-space
4
+ tool, calculate the occurrence graph and the SCC graph, paste an ASK-CTL script
5
+ into an auxiliary box and evaluate it. CPNCheck answers the same questions from
6
+ Python, on the very ``.cpn`` file CPN Tools would open, and cross-checks its
7
+ answers against CPN Tools itself.
8
+
9
+ >>> from cpncheck import check_cpn
10
+ >>> result = check_cpn("model.cpn", properties="AG !dead")
11
+ >>> print(result.report())
12
+
13
+ Layers, bottom up:
14
+
15
+ ============================ ====================================================
16
+ :mod:`cpncheck.net` parse the ``.cpn`` XML
17
+ :mod:`cpncheck.coloured` compile the CPN ML fragment; flatten; fire
18
+ :mod:`cpncheck.statespace` occurrence graph, SCCs, home markings
19
+ :mod:`cpncheck.reduction` stubborn-set reduced graphs (partial-order reduction)
20
+ :mod:`cpncheck.sweepline` the generalised sweep-line method
21
+ :mod:`cpncheck.equivalence` quotient graphs; time equivalence
22
+ :mod:`cpncheck.symmetry` symmetry reduction
23
+ :mod:`cpncheck.combined` several reductions at once
24
+ :mod:`cpncheck.query` user properties written as text
25
+ :mod:`cpncheck.askctl_input` ASK-CTL query files, with ASKCTL.sml's semantics
26
+ :mod:`cpncheck.ctl` CTL fixpoints over the occurrence graph
27
+ :mod:`cpncheck.properties` the property catalog
28
+ :mod:`cpncheck.result` verdicts and reports (text, JSON, LaTeX)
29
+ :mod:`cpncheck.checker` the four techniques, and the public entry points
30
+ :mod:`cpncheck.oracle` run CPN Tools 4.0.1 headless and compare
31
+ :mod:`cpncheck.mutation` mutants: every property must be able to fail
32
+ ============================ ====================================================
33
+
34
+ Examples, one feature per script: ``examples/`` in the repository.
35
+ """
36
+
37
+ from .checker import CPNModelChecker, check_cpn
38
+ from .coloured import ColouredNet, UnsupportedNet
39
+ from .net import CPNNet
40
+ from .properties import METHODS, Property, catalog
41
+ from .result import ModelCheckResult, PropertyResult, compare_results
42
+ from .statespace import DEFAULT_MAX_NODES, StateSpace, build_state_space
43
+
44
+ __version__ = "0.2.0"
45
+
46
+ __all__ = [
47
+ "CPNModelChecker",
48
+ "CPNNet",
49
+ "ColouredNet",
50
+ "DEFAULT_MAX_NODES",
51
+ "METHODS",
52
+ "ModelCheckResult",
53
+ "catalog",
54
+ "Property",
55
+ "PropertyResult",
56
+ "StateSpace",
57
+ "UnsupportedNet",
58
+ "__version__",
59
+ "build_state_space",
60
+ "check_cpn",
61
+ "compare_results",
62
+ ]
cpncheck/askctl.py ADDED
@@ -0,0 +1,352 @@
1
+ """Rendering a formula as an ASK-CTL query for CPN Tools.
2
+
3
+ The same :mod:`cpncheck.formula` tree that :mod:`cpncheck.checker` evaluates in
4
+ Python is rendered here as CPN ML, so the two engines are asked *the same*
5
+ question by construction. Before this existed the Python check and the ASK-CTL
6
+ script were written by hand side by side, and nothing but care kept them in
7
+ agreement.
8
+
9
+ Correspondence with ASK-CTL (Cheng, Christensen & Mortensen; the ``ASKCTL``
10
+ library shipped with CPN Tools):
11
+
12
+ ========================= ====================================================
13
+ ``Dead`` ``DEAD``, declared as ``NOT(MODAL(TT))``
14
+ ``Not`` / ``And`` / ``Or`` ``NOT`` / ``AND`` / ``OR`` (binary, folded right)
15
+ ``AG`` ``INV``
16
+ ``EF`` ``POS``
17
+ ``EU`` ``EXIST_UNTIL``
18
+ ``AF`` (textbook) ``AFmax``, declared below
19
+ ``AF`` (ASK-CTL reading) ``EV``
20
+ atomic propositions ``NF("name", fn cpnq_n => ...)`` over the node's marking
21
+ ``ForEach`` folded into ``AND``, one conjunct per member
22
+ ========================= ====================================================
23
+
24
+ ``AF`` needs the declared helper because ASK-CTL's ``EV`` is vacuously true at a
25
+ dead marking: ``EV(phi)`` means ``AF(phi or dead)``, so a net that stops before
26
+ ``phi`` would satisfy it. ``AFmax phi`` adds the missing conjunct.
27
+ """
28
+
29
+ import collections
30
+ import re
31
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
32
+
33
+ from . import formula as F
34
+
35
+ __all__ = ["MLProgram", "UnsupportedFormula"]
36
+
37
+
38
+ # Exception raised when a formula has no ASK-CTL rendering.
39
+ class UnsupportedFormula(Exception):
40
+ """The formula uses a construct this renderer cannot express in CPN ML."""
41
+
42
+
43
+ # Class rendering formulas as ASK-CTL and collecting the declarations they need.
44
+ class MLProgram:
45
+ """Builds the declarations and the queries of one CPN Tools script.
46
+
47
+ Rendering a formula may require declarations -- a predicate over arcs for a
48
+ transition group, the ``AFmax`` helper -- which are emitted once and in
49
+ dependency order. Render every formula first, then read
50
+ :meth:`declarations`.
51
+
52
+ Attributes:
53
+ net (ColouredNet): The net being rendered, for page and element names.
54
+ """
55
+
56
+ # Method to build an empty program.
57
+ def __init__(self, net):
58
+ """Create the renderer.
59
+
60
+ Args:
61
+ net (ColouredNet): The flattened net.
62
+ """
63
+ self.net = net
64
+ self._decls: List[str] = []
65
+ self._seen = set()
66
+ self._groups: Dict[tuple, str] = {}
67
+
68
+ # Method to emit a declaration once.
69
+ def declare(self, text: str) -> None:
70
+ """Add a declaration, ignoring repeats.
71
+
72
+ Args:
73
+ text (str): The CPN ML declaration.
74
+ """
75
+ if text not in self._seen:
76
+ self._seen.add(text)
77
+ self._decls.append(text)
78
+
79
+ # Method to return every declaration emitted so far.
80
+ def declarations(self) -> str:
81
+ """The declarations the rendered queries depend on.
82
+
83
+ Returns:
84
+ str: One declaration per line, in dependency order.
85
+ """
86
+ return "\n".join(self._decls)
87
+
88
+ # ------------------------------------------------------------------
89
+ # Names
90
+ # ------------------------------------------------------------------
91
+
92
+ # Method to render the token count of one place as an ML function.
93
+ def mark(self, pidx: int) -> str:
94
+ """A function from a node to the number of tokens of a place.
95
+
96
+ Args:
97
+ pidx (int): Place index.
98
+
99
+ Returns:
100
+ str: ``(fn cpnq_n => size (Mark.Page'Place 1 cpnq_n))``.
101
+ """
102
+ return f"(fn cpnq_n => size ({self.multiset(pidx)}))"
103
+
104
+ # Method to render a list of token counters.
105
+ def marks(self, places: Sequence[int]) -> str:
106
+ """An ML list of the counting functions of several places.
107
+
108
+ Args:
109
+ places (sequence): Place indices.
110
+
111
+ Returns:
112
+ str: ``[f1, f2, ...]``.
113
+ """
114
+ return "[" + ", ".join(self.mark(p) for p in places) + "]"
115
+
116
+ # Method to render the marking of one place as an ML multiset.
117
+ def multiset(self, pidx: int) -> str:
118
+ """The marking of a place, as the ML multiset expression.
119
+
120
+ Args:
121
+ pidx (int): Place index.
122
+
123
+ Returns:
124
+ str: ``Mark.Page'Place 1 cpnq_n``.
125
+ """
126
+ p = self.net.places[pidx]
127
+ return f"Mark.{_ml_of(p)} {p.get('instance', 1)} cpnq_n"
128
+
129
+ # Method to declare and name an arc predicate for a transition group.
130
+ def group(self, transitions: Sequence[int]) -> str:
131
+ """A predicate telling whether an arc belongs to a transition group.
132
+
133
+ Args:
134
+ transitions (sequence): Transition indices.
135
+
136
+ Returns:
137
+ str: The name of the declared predicate.
138
+
139
+ Raises:
140
+ UnsupportedFormula: When the group is empty.
141
+ """
142
+ key = tuple(sorted(transitions))
143
+ if not key:
144
+ raise UnsupportedFormula("an empty transition group has no "
145
+ "ASK-CTL rendering")
146
+ if key in self._groups:
147
+ return self._groups[key]
148
+ name = f"isG{len(self._groups)}"
149
+ cases = " | ".join(
150
+ f"Bind.{_ml_of(self.net.transitions[ti])} "
151
+ f"({self.net.transitions[ti].get('instance', 1)}, _) => true"
152
+ for ti in key)
153
+ self.declare("fun enCount cpnq_isT cpnq_n = length "
154
+ "(List.filter cpnq_isT (OutArcs cpnq_n));")
155
+ self.declare(f"fun {name} cpnq_a = case ArcToBE cpnq_a of {cases} "
156
+ "| _ => false;")
157
+ self._groups[key] = name
158
+ return name
159
+
160
+ # ------------------------------------------------------------------
161
+ # Rendering
162
+ # ------------------------------------------------------------------
163
+
164
+ # Method to render one formula as an ASK-CTL expression.
165
+ def render(self, phi, ctx, semantics: str = "textbook") -> str:
166
+ """Translate a formula into an ASK-CTL expression.
167
+
168
+ Args:
169
+ phi (Formula): The formula.
170
+ ctx (Context): The context the selectors resolve against.
171
+ semantics (str): ``"textbook"`` or ``"askctl"``, for ``AF``.
172
+
173
+ Returns:
174
+ str: The ASK-CTL expression.
175
+
176
+ Raises:
177
+ UnsupportedFormula: For a formula with no rendering.
178
+ """
179
+ render = lambda x: self.render(x, ctx, semantics) # noqa: E731
180
+
181
+ if getattr(phi, "script", None) is not None:
182
+ # An ASK-CTL query as the user wrote it, after its file.
183
+ if phi.script.declarations:
184
+ self.declare(phi.script.declarations)
185
+ return phi.text
186
+
187
+ if isinstance(phi, F.Dead):
188
+ self.declare("val DEAD = NOT(MODAL(TT));")
189
+ return "DEAD"
190
+
191
+ if isinstance(phi, F.Marked):
192
+ places = ctx.select(phi.selector)
193
+ body = (f"size ({self.multiset(places[0])}) > 0" if len(places) == 1
194
+ else f"List.exists (fn cpnq_f => cpnq_f cpnq_n > 0) "
195
+ f"{self.marks(places)}")
196
+ return f'NF("marked", fn cpnq_n => {body})'
197
+
198
+ if isinstance(phi, F.AllMarked):
199
+ places = ctx.select(phi.selector)
200
+ return ('NF("all marked", fn cpnq_n => List.all '
201
+ '(fn cpnq_f => cpnq_f cpnq_n > 0) '
202
+ f'{self.marks(places)})')
203
+
204
+ if isinstance(phi, F.MaxTokens):
205
+ places = ctx.select(phi.selector)
206
+ return (f'NF("at most {phi.limit}", fn cpnq_n => List.all '
207
+ f'(fn cpnq_f => cpnq_f cpnq_n <= {phi.limit}) '
208
+ f'{self.marks(places)})')
209
+
210
+ if isinstance(phi, F.TokensIn):
211
+ places = ctx.select(phi.selector)
212
+ domain = "[" + ", ".join(_ml_literal(v)
213
+ for v in sorted(phi.domain)) + "]"
214
+ parts = [f"(let val cpnq_m = {self.multiset(p)} in size cpnq_m = "
215
+ f"List.foldl (fn (cpnq_k, cpnq_acc) => cpnq_acc + "
216
+ f"cf (cpnq_k, cpnq_m)) 0 {domain} end)"
217
+ for p in places]
218
+ return ('NF("in domain", fn cpnq_n => ' + " andalso ".join(parts) + ")")
219
+
220
+ if isinstance(phi, F.TokenCount):
221
+ places = ctx.select(phi.selector)
222
+ total = " + ".join(f"{self.mark(p)} cpnq_n" for p in places) or "0"
223
+ return (f'NF("tokens", fn cpnq_n => ({total}) {phi.op} {phi.limit})')
224
+
225
+ if isinstance(phi, F.Holds):
226
+ places = ctx.select(phi.selector)
227
+ parts = " orelse ".join(f"cf ({phi.ml_text}, {self.multiset(p)}) > 0"
228
+ for p in places) or "false"
229
+ return f'NF("has", fn cpnq_n => {parts})'
230
+
231
+ if isinstance(phi, F.EnabledTally):
232
+ name = self.group(ctx.select(phi.selector))
233
+ return (f'NF("enabled count", fn cpnq_n => enCount {name} cpnq_n '
234
+ f'{phi.op} {phi.limit})')
235
+
236
+ if isinstance(phi, F.Enabled):
237
+ name = self.group(ctx.select(phi.selector))
238
+ return f'NF("enabled", fn cpnq_n => enCount {name} cpnq_n > 0)'
239
+
240
+ if isinstance(phi, F.EnabledCount):
241
+ name = self.group(ctx.select(phi.selector))
242
+ return (f'NF("at most {phi.limit} enabled", fn cpnq_n => enCount '
243
+ f'{name} cpnq_n <= {phi.limit})')
244
+
245
+ if isinstance(phi, F.Not):
246
+ return f"NOT({render(phi.operand)})"
247
+
248
+ if isinstance(phi, F.And):
249
+ return _fold("AND", [render(o) for o in phi.operands])
250
+
251
+ if isinstance(phi, F.Or):
252
+ return _fold("OR", [render(o) for o in phi.operands])
253
+
254
+ if isinstance(phi, F.AG):
255
+ return f"INV({render(phi.operand)})"
256
+
257
+ if isinstance(phi, F.EF):
258
+ return f"POS({render(phi.operand)})"
259
+
260
+ if isinstance(phi, F.EU):
261
+ return f"EXIST_UNTIL({render(phi.hold)}, {render(phi.target)})"
262
+
263
+ if isinstance(phi, F.AF):
264
+ inner = render(phi.operand)
265
+ if (phi.semantics or semantics) == "askctl":
266
+ return f"EV({inner})"
267
+ self.declare("val DEAD = NOT(MODAL(TT));")
268
+ self.declare("fun AFmax phi = AND(EV(phi), "
269
+ "NOT(EXIST_UNTIL(NOT(phi), AND(DEAD, NOT(phi)))));")
270
+ return f"AFmax ({inner})"
271
+
272
+ if isinstance(phi, F.ForEach):
273
+ parts = [render(f) for _label, f in phi.instances(ctx)]
274
+ if not parts:
275
+ return "TT"
276
+ return _fold("AND", parts)
277
+
278
+ raise UnsupportedFormula(
279
+ f"{type(phi).__name__} has no ASK-CTL rendering")
280
+
281
+ # Method to render a formula as a complete, string-valued query.
282
+ def query(self, phi, ctx, semantics: str = "textbook") -> str:
283
+ """A query evaluating the formula at the initial marking.
284
+
285
+ Args:
286
+ phi (Formula): The formula.
287
+ ctx (Context): The context the selectors resolve against.
288
+ semantics (str): ``"textbook"`` or ``"askctl"``.
289
+
290
+ Returns:
291
+ str: A string-valued CPN ML expression.
292
+ """
293
+ return (f"Bool.toString (eval_node ({self.render(phi, ctx, semantics)})"
294
+ " InitNode)")
295
+
296
+
297
+ # Function to fold a binary ASK-CTL connective over a list.
298
+ def _fold(op: str, parts: List[str]) -> str:
299
+ """Combine expressions with a binary connective.
300
+
301
+ Args:
302
+ op (str): ``"AND"`` or ``"OR"``.
303
+ parts (list): The rendered operands.
304
+
305
+ Returns:
306
+ str: The folded expression.
307
+ """
308
+ if not parts:
309
+ return "TT" if op == "AND" else "NOT(TT)"
310
+ out = parts[-1]
311
+ for part in reversed(parts[:-1]):
312
+ out = f"{op}({part}, {out})"
313
+ return out
314
+
315
+
316
+ # Function to give the CPN ML path of a flattened place or transition.
317
+ def _ml_of(element) -> str:
318
+ """``Page'Name`` as CPN Tools spells it in ``Mark.`` and ``Bind.``.
319
+
320
+ Args:
321
+ element (dict): A place or transition of the flattened net.
322
+
323
+ Returns:
324
+ str: The path; built from the names when the record has none.
325
+ """
326
+ if element.get("ml"):
327
+ return element["ml"]
328
+ clean = lambda s: re.sub(r"[^A-Za-z0-9_']", "_", s.strip()) # noqa: E731
329
+ return f"{clean(element['page'])}'{clean(element['name'])}"
330
+
331
+
332
+ # Function to render a Python token value as a CPN ML literal.
333
+ def _ml_literal(value) -> str:
334
+ """Write a token value the way CPN ML spells it.
335
+
336
+ Args:
337
+ value: The token value.
338
+
339
+ Returns:
340
+ str: The literal.
341
+
342
+ Raises:
343
+ UnsupportedFormula: For a value with no literal form.
344
+ """
345
+ if isinstance(value, bool):
346
+ return "true" if value else "false"
347
+ if isinstance(value, int):
348
+ return str(value) if value >= 0 else f"~{-value}"
349
+ if isinstance(value, float):
350
+ text = repr(value)
351
+ return text.replace("-", "~") if text.startswith("-") else text
352
+ raise UnsupportedFormula(f"no CPN ML literal for {value!r}")