petri_net_nn.coverability¶
coverability ¶
Karp-Miller coverability analysis for unbounded Petri nets.
The bisimulation and soundness checkers in PETRA assume the net is bounded — that the reachable marking space is finite, so BFS terminates and partition refinement applies. Most well-designed business processes are bounded by construction, but some are not, and a process that quietly accumulates work at one place is a production-incident waiting to surface. This module answers the prior question: is this net bounded at all, and if not, which places are the ones blowing up?
The algorithm is the classical Karp-Miller construction. Starting
from the initial marking, every reachable marking is generated by
BFS. When a newly-generated marking M' strictly covers an
ancestor M on its path from the root — meaning M' >= M
componentwise and M' != M — each place where M' exceeds
M is replaced with the sentinel ω ("arbitrarily many").
The tree is finite by Dickson's lemma: there is no infinite
antichain on N^k, and once a place is ω-marked, subsequent
firings can only leave it ω or remove tokens it never runs out
of, so branching is contained.
A place is unbounded iff some node in the coverability tree
carries ω at it. The :class:CoverabilityReport returned by
:func:coverability_graph pins exactly which places those are,
the per-place upper bound for the bounded ones, and a list of
ω-marking witnesses.
Scope and limits¶
-
Plain Petri nets, arc weights, multi-token markings: fully decidable. The Karp-Miller algorithm gives the exact answer.
-
Inhibitor arcs: handled conservatively. An ω-marked inhibitor place is treated as non-empty, so the inhibitor arc blocks the transition. This is sound (no real firings are missed) but may overreport unboundedness in some pathological cases. The reason is fundamental, not a tool limitation: inhibitor-arc Petri nets are Turing-complete (Minsky 1967), so exact coverability would solve the halting problem. The BUSINESS_ANALYST_GUIDE §17 has a long-form treatment of why this boundary is mathematical rather than a missing feature.
-
Coloured tokens, durations, rates, guards: these Phase 9 extensions affect the trained network and the coloured token-game; coverability operates on the raw discrete structural net (places, transitions, flow, arc weights, inhibitors) and ignores them. A coloured net that is unbounded structurally will still be reported as unbounded; a net whose unboundedness depends on coloured-token routing logic may not be analysable here.
The max_nodes guard (default 10,000) raises ValueError if
the coverability tree exceeds the cap. Karp-Miller terminates in
principle for every Petri net, but a degenerate net with many
places and frequent ω introductions can still produce a large
tree.
CoverabilityReport
dataclass
¶
Outcome of a Karp-Miller coverability analysis.
The headline field is :attr:is_bounded; the rest pin which
places are unbounded, how high the bounded ones can go, and
which specific markings witness the unboundedness.
Attributes¶
is_bounded
True iff no place in any node of the coverability tree
carries ω. A bounded net has a finite reachable marking
space — exactly the precondition the rest of PETRA's
verification checks assume.
unbounded_places
Names of the places that can hold arbitrarily many tokens.
Sorted alphabetically. Empty iff the net is bounded.
place_bounds
Per-place upper bound on the token count across the
coverability tree. OMEGA for unbounded places, an
integer for bounded ones. The integer bound is attained —
some reachable marking actually puts that many tokens at
the place. Every place declared on the net appears here
(with bound 0 if no reachable marking ever puts a token
on it — those places would be flagged by the soundness
checker's dead-transition pass too).
omega_markings
Coverability nodes that carry ω at one or more places.
These are the witnesses for unboundedness: each one
records "the net can reach a state where these places
hold arbitrarily many tokens." Sorted for deterministic
output. Empty iff the net is bounded.
summary ¶
One-line human-readable digest. "bounded" on a
bounded net; "unbounded at N place(s): p1, p2, ..." on
an unbounded one. Useful for log lines and assertion
messages.
Source code in petri_net_nn/coverability.py
coverability_graph ¶
Compute the Karp-Miller coverability tree of net and
return a :class:CoverabilityReport.
Identifies which places (if any) can hold arbitrarily many tokens — i.e., are unbounded. Sound and exact on nets without inhibitor arcs; conservative (may overreport unboundedness) on nets that use them. See the module-level docstring for the reasoning behind the conservative treatment.
Parameters¶
net
The PetriNet to analyse. Doesn't need to be bounded —
that's the whole point. The initial marking provides the
root of the coverability tree.
max_nodes
Tree-size cap; raises ValueError if the visited set
exceeds it. Defensive guard: Karp-Miller terminates in
principle, but degenerate inputs can still produce large
trees.
Returns¶
CoverabilityReport
is_bounded flag, list of unbounded place names, the
per-place bounds, and the ω-marking witnesses for any
unboundedness.
Source code in petri_net_nn/coverability.py
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | |
is_bounded ¶
Convenience wrapper: True iff every place in net is
bounded. Equivalent to coverability_graph(net).is_bounded
but spelled at the use-site as a one-liner question.
Useful as a precondition guard before calling any of the other
verification checks (bisimulation, soundness, CTL) that assume
the reachable state space is finite — those checks raise
ValueError from the max_states guard rather than
silently going wrong, but is_bounded lets the caller catch
the case explicitly.