Transitions¶
Transitions fire work over the thread pool; arcs connect them to places.
cpnx.Transition
dataclass
¶
A CPN transition that fires when all its input places are enabled.
In CPN formalism a transition has:
- Input arcs whose
key/filterdetermine which tokens are consumed - Output arcs whose
conditiondetermines whether tokens are produced - A guard — a boolean predicate over the binding that must hold for the transition to be enabled (evaluated before the action runs)
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Unique identifier within a |
inputs |
list[InputArc]
|
Input arcs (place to transition). |
outputs |
list[OutputArc]
|
Output arcs (transition to place). |
action |
Callable[[list[Token]], list[Token]]
|
Callable consuming input tokens and returning output tokens. Runs on the thread pool outside the engine lock. |
guard |
Callable[[list[Token]], bool] | None
|
CPN transition guard — a |
priority |
int
|
Lower value fires first when multiple transitions are enabled. Defaults to 10. |
action_timeout_secs |
float | None
|
Maximum wall-clock seconds the action may run. When This does not kill the underlying OS thread. The timed-out action
continues running in the background until it completes or the process exits;
its return value is silently discarded. Callers must apply native I/O
timeouts inside their actions (e.g. |
max_retries |
int | None
|
Maximum number of times to retry the transition action on failure
before dead-lettering the data tokens.
|
binding_policy |
BindingPolicy | None
|
How the engine resolves which input tokens bind this transition
when checking enablement — see |
binding_priority_key |
Callable[[list[Token]], object] | None
|
Sort key used only under Performance / lock discipline: unlike a callable |
Source code in src/cpnx/transitions.py
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 | |
cpnx.SubstitutionTransition
dataclass
¶
Bases: Transition
A Transition that encapsulates an entire sub-PetriNet (Hierarchical CPN).
Unlike a plain Transition, whose action is an arbitrary callable, a
SubstitutionTransition delegates firing to a nested PetriNet (the
subnet): named port places inside the subnet are bound to named socket places in the
parent net via port_socket_map, and subnet_deadline_secs bounds how long the subnet
is given to reach quiescence. The child subnet is fully insulated from the parent net —
it carries no reference to its parent. Communication occurs strictly through the
port_socket_map.
A subnet instance may only be wrapped by one SubstitutionTransition at a
time; this is tracked process-wide via a weak set. Attempting to wrap the same subnet
twice raises ValueError. Constructing with a non-dict port_socket_map, or with
non-string port/socket names, raises TypeError. Referencing a port place that does not
exist in the subnet raises ValueError.
Attributes:
| Name | Type | Description |
|---|---|---|
subnet |
PetriNet
|
The nested |
port_socket_map |
dict[str, str]
|
Mapping of port place names (in |
subnet_deadline_secs |
float
|
Maximum wall-clock seconds allowed for the subnet to reach
quiescence when this transition fires. Defaults to |
Source code in src/cpnx/transitions.py
cpnx.InputArc
dataclass
¶
Describe how a transition consumes tokens from one input place.
In CPN formalism an input arc carries an arc expression — a function that, given the current tokens in the place, determines which tokens are consumed and in what order. cpnx splits that inscription into its two honest halves, both per token:
filter— eligibility: which tokens this arc may consume at all;key— order: a total order over the eligible tokens, min-first.
The engine applies filter, orders what survives by key (ascending, ties broken by
insertion order), and consumes the first count. With neither set the arc is plain
FIFO. Per-token parameters are what make the selection indexable, which an opaque
list[Token] -> list[Token] transform can never be — see
docs/adr/0004-arc-selection-key-filter.md.
Attributes:
| Name | Type | Description |
|---|---|---|
place |
str
|
Name of the source place. |
count |
int
|
Number of tokens to consume. Ignored when |
consume_all |
bool
|
Drain the entire place atomically. Defaults to |
settle_secs |
float
|
Wait for no new arrivals for this many seconds before
consuming. Defaults to |
key |
Callable[[Token], object] | None
|
Per-token sort key — a pure |
filter |
Callable[[Token], bool] | None
|
Per-token eligibility predicate — a pure |
Warning
consume_all=True ignores both key and filter. A draining arc takes every
available token, in FIFO order, whatever the selection callables say — a token the
filter rejects is still consumed. This preserves the pre-key/filter
behavior of the arc inscription it replaced, but it is a genuine footgun: filter
reads as a declaration of eligibility, and under consume_all it is not one. Do
not combine them expecting "drain everything eligible"; that pattern is not
supported today. To drain only eligible tokens, use a large count rather than
consume_all.
Because that combination is always a mistake rather than a style choice,
constructing (or reassigning into) such an arc emits a UserWarning naming the
ignored parameters. Silence it with
warnings.filterwarnings("ignore", message="consume_all=True ignores") if you
genuinely mean "drain everything, selection notwithstanding".
Warning
On a deep place, make your key/filter closed-world (certified) — or pay a
quadratic drain. A certified selection callable is served from a persistent
(key, seq) index maintained on the place, so the drain stays roughly linear in
place depth. An uncertified one cannot be indexed — keying happens on the
deposit() path, which cannot host an unbounded callable — so every enabling
check re-reads and re-sorts the whole available pool and dispatches the
callable through the timeout-bounded executor once per token. Both costs are
per-token-per-firing, so draining a deep place is O(N²): worst-case lock-hold
for the arc is len(place) * expr_timeout_secs, and benchmarks/bench_station_costs.py
measures an uncertified key at over 100× a certified key computing the
identical order at depth 2000 — a gap that widens with depth. A filter is
worse to get wrong than a key: a single uncertified filter disqualifies the
whole arc from indexing even when the key is certified, because applying an
uncertified predicate after a capped index read could silently under-select.
To certify, close over immutable values — never read mutable state at call
time. A callable certifies (see [cpnx.certification]) only when it draws on a
fixed vocabulary and closes over nothing mutable. In practice: read configuration
at construction and capture the value. A callable that reads a module global, an
instance attribute, or any list/dict/set on each call does not certify —
that is exactly the pattern that lands on the O(N²) path. Captured values must be
immutable leaves (numbers, strings, bytes, bool, None) or tuple/frozenset
thereof — a frozenset certifies, a set does not::
cutoff = config["vip_cutoff"] # an int, read once at construction
key = lambda t: (t.payload["spend"] < cutoff, t.created_at) # closes over an int -> certifies
If the ordering genuinely depends on state that changes at run time — a live priority a human retunes mid-run — it cannot certify by construction, and the quadratic cost is intrinsic, not a bug: an index needs a key it can compute once, at deposit, and a value that changes afterward would sort already-deposited tokens wrong. There is no engine fix for that case. Restructure so the key is closed-world (snapshot the state into an immutable at construction and rebuild the arc when it changes), or accept the O(N²) on a shallow place only.
Note
Both callables are purity-verified at assignment, and each carries its own inline-safe flag: a certified callable runs inline under the engine lock, an uncertified one on the timeout-bounded expression pool (the source of the cost in the warning above).
Separately, expr_timeout_secs bounds a key's extraction, not the
comparisons between the values it returns — the sort runs inline under the
engine lock. Return plain comparables (numbers, strings, tuples thereof); a value
with a slow or diverging __lt__ can hold the lock past any timeout.
Example
Source code in src/cpnx/transitions.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 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 | |
cpnx.OutputArc
dataclass
¶
Describe how a transition deposits tokens into one output place.
In CPN formalism an output arc carries an arc expression — a function evaluated
against the transition's output tokens that determines whether tokens flow along this
arc. On the output side that inscription is, in practice, always a boolean activation
predicate, so cpnx names it condition — a different mechanism from the input side's
token selection (see InputArc and
docs/adr/0004-arc-selection-key-filter.md).
Attributes:
| Name | Type | Description |
|---|---|---|
place |
str
|
Name of the target place. |
count |
int
|
Number of tokens to deposit. Defaults to 1. |
condition |
Callable[[list[Token]], bool] | None
|
Arc activation predicate. Receives the list of non-resource
output tokens returned by the action; the arc is skipped
(no tokens deposited) when it returns |
Source code in src/cpnx/transitions.py
on_color
classmethod
¶
Build an OutputArc that only fires for a matching first token color.
The returned arc's condition is a callable that checks whether the action's
output tokens are non-empty and the first token's color equals color. It closes
over color (an immutable string), so it certifies for inline evaluation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
color
|
str
|
The |
required |
place
|
str
|
Name of the target place. |
required |
count
|
int
|
Number of tokens to deposit when the arc fires. Defaults to 1. |
1
|
Returns:
| Type | Description |
|---|---|
OutputArc
|
A new |
OutputArc
|
when the first token in the action's output has color |
Source code in src/cpnx/transitions.py
BindingPolicy selects how a transition resolves which input tokens bind it — the legacy leading-token check, or a deterministic-complete binding search.
cpnx.BindingPolicy
¶
Bases: Enum
Strategy for choosing which input tokens bind a transition when it is enabled.
In CPN theory a transition is enabled if any assignment of place tokens (a
binding) satisfies its guard. cpnx historically tested only the head of each
input place (FIFO, or reordered by InputArc.key), which
causes head-of-line blocking: a place holding [A, B] whose guard wants B
reports the transition disabled, because only A is ever tested. BindingPolicy
selects how the engine resolves that binding. See the design record in
docs/adr/0001-combinatorial-binding-search.md.
Attributes:
| Name | Type | Description |
|---|---|---|
LEGACY |
Test only the first Deprecated. |
|
FIRST |
Search input-token combinations in a stable insertion order and select
the first combination whose guard is satisfied. Complete (finds a valid
binding if one exists anywhere in the place, fixing head-of-line blocking)
and deterministic (the same marking always yields the same binding). When
the transition has no guard, this is identical to |
|
RANDOM |
Enumerate the satisfying combinations and select one uniformly at
random. Reproducible when the owning |
|
PRIORITY |
Enumerate the satisfying combinations and select the one minimizing
|
Note
The search enumerates the Cartesian product of each input arc's count-sized token
combinations, varying the last arc in Transition.inputs fastest. Consequences
for tuning:
- Resource arcs inflate the space. A
ResourcePlace/PacedResourcePlacepermit arc contributesC(capacity, count)interchangeable options that usually give the guard the same answer, so they can consumebinding_search_limiton redundant permutations. List resource arcs before data arcs inTransition.inputsso the data dimension (the one that actually changes the guard result) varies first, and/or raisebinding_search_limit. - For
FIRSTthe first binding yielded is exactlyLEGACY's head selection, soFIRSTis a strict superset ofLEGACY. RANDOM/PRIORITYmust scan the whole (bounded) candidate set, so they do not short-circuit and are typically costlier thanFIRST. If the candidate space exceedsbinding_search_limit, they select over the firstlimitcandidates only (a truncated prefix), firingon_binding_search_exhausted. If that prefix contains no satisfying binding, the transition is treated as disabled for that check — it can stall exactly likeFIRST, not just fire over a smaller set.