cpnx 0.1.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 (40) hide show
  1. cpnx-0.1.0/.agents/AGENTS.md +26 -0
  2. cpnx-0.1.0/.github/workflows/ci.yml +24 -0
  3. cpnx-0.1.0/.gitignore +37 -0
  4. cpnx-0.1.0/LICENSE +21 -0
  5. cpnx-0.1.0/Makefile +14 -0
  6. cpnx-0.1.0/PKG-INFO +327 -0
  7. cpnx-0.1.0/README.md +304 -0
  8. cpnx-0.1.0/examples/api_rate_limit.py +34 -0
  9. cpnx-0.1.0/examples/etl_pipeline.py +68 -0
  10. cpnx-0.1.0/examples/gpu_pipeline.py +31 -0
  11. cpnx-0.1.0/pyproject.toml +46 -0
  12. cpnx-0.1.0/src/cpnx/__init__.py +22 -0
  13. cpnx-0.1.0/src/cpnx/engine.py +806 -0
  14. cpnx-0.1.0/src/cpnx/places.py +473 -0
  15. cpnx-0.1.0/src/cpnx/py.typed +1 -0
  16. cpnx-0.1.0/src/cpnx/sandbox.py +177 -0
  17. cpnx-0.1.0/src/cpnx/tokens.py +129 -0
  18. cpnx-0.1.0/src/cpnx/transitions.py +143 -0
  19. cpnx-0.1.0/src/cpnx/visualization.py +74 -0
  20. cpnx-0.1.0/tests/test_backpressure.py +117 -0
  21. cpnx-0.1.0/tests/test_batch.py +117 -0
  22. cpnx-0.1.0/tests/test_chronology.py +140 -0
  23. cpnx-0.1.0/tests/test_color_sets.py +139 -0
  24. cpnx-0.1.0/tests/test_concurrent.py +156 -0
  25. cpnx-0.1.0/tests/test_deep_review_fixes.py +390 -0
  26. cpnx-0.1.0/tests/test_encapsulation.py +225 -0
  27. cpnx-0.1.0/tests/test_engine.py +268 -0
  28. cpnx-0.1.0/tests/test_error.py +282 -0
  29. cpnx-0.1.0/tests/test_guards.py +152 -0
  30. cpnx-0.1.0/tests/test_input_arc_expression.py +141 -0
  31. cpnx-0.1.0/tests/test_optimizations.py +108 -0
  32. cpnx-0.1.0/tests/test_places.py +130 -0
  33. cpnx-0.1.0/tests/test_priority.py +157 -0
  34. cpnx-0.1.0/tests/test_public_api.py +69 -0
  35. cpnx-0.1.0/tests/test_pure_evaluation.py +161 -0
  36. cpnx-0.1.0/tests/test_resource.py +117 -0
  37. cpnx-0.1.0/tests/test_routing.py +125 -0
  38. cpnx-0.1.0/tests/test_threshold.py +142 -0
  39. cpnx-0.1.0/tests/test_tokens.py +59 -0
  40. cpnx-0.1.0/tests/test_visualization.py +121 -0
@@ -0,0 +1,26 @@
1
+ # Petriq Agent Instructions & Rules
2
+
3
+ These project-scoped guidelines apply to all AI agents working on the `cpnx` codebase.
4
+
5
+ ---
6
+
7
+ ## 1. Concurrency & Thread Safety Rules
8
+ * **No Locks During Callbacks**: Never invoke user-supplied callbacks (such as `on_transition_fired`, `on_error`, or `on_token_deposited`) while holding the internal engine lock (`self._lock`). Doing so poses high risks of re-entrant deadlocks.
9
+ * **Encapsulation Invariant**: Avoid direct accesses to private fields of places (e.g., `place._tokens` or `place._lock`) inside the engine code. Always delegate to thread-safe public interfaces like `len(place)` or explicit query methods.
10
+ * **Lock Re-entrancy Safety**: The place and engine locks are standard `threading.Lock` instances (non-reentrant). Subclasses (like `PacedResourcePlace`) overriding retrieval/deposit methods must manipulate inner properties directly under their own lock instead of using nested `super()` calls that would attempt to acquire the lock a second time and deadlock.
11
+
12
+ ---
13
+
14
+ ## 2. Resource & Memory Safety Rules
15
+ * **No Token Leaks**:
16
+ * Always wrap `self._executor.submit()` calls in try-except blocks to catch executor failures (e.g., pool shutdown) and restore consumed input tokens back to their source places.
17
+ * Surplus resource tokens remaining in transition queues must be returned to their original source places upon successful transition execution.
18
+ * **Prune Cooldowns**: Ensure custom resource places (e.g., `PacedResourcePlace`) clean up internal cooldown mapping dictionaries when tokens are retrieved.
19
+
20
+ ---
21
+
22
+ ## 3. Formatting, Linting & Python Styling
23
+ * **Compliance Checks**: Before proposing any change, always run `make format` and `make lint` to ensure compliance with Ruff formatting rules.
24
+ * **Testing Requirement**: Always run `make test` and ensure all unit tests pass before concluding a task. Add new tests for any modified or new behavior.
25
+ * **Python Target**: Code should align with Python 3.10+ conventions (Union typing `A | B` rather than `Union[A, B]`).
26
+ * **Docstring Guidelines**: Follow PEP 257 docstring conventions for all public classes, methods, and functions.
@@ -0,0 +1,24 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
12
+ include:
13
+ - python-version: "3.14"
14
+ experimental: true
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ allow-prereleases: true
21
+ - run: pip install -e ".[dev]"
22
+ - run: ruff check src/ tests/
23
+ - run: pytest tests/ -v
24
+ continue-on-error: ${{ matrix.experimental == true }}
cpnx-0.1.0/.gitignore ADDED
@@ -0,0 +1,37 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.pyo
6
+ *.pyd
7
+ *.egg-info/
8
+ *.egg
9
+ dist/
10
+ build/
11
+ .eggs/
12
+ *.so
13
+
14
+ # Test / lint caches
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ .mypy_cache/
18
+ .hypothesis/
19
+ htmlcov/
20
+ .coverage
21
+ coverage.xml
22
+ *.cover
23
+
24
+ # macOS
25
+ .DS_Store
26
+ .AppleDouble
27
+ .LSOverride
28
+
29
+ # Editors
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # Env vars
37
+ .env
cpnx-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philgresh
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.
cpnx-0.1.0/Makefile ADDED
@@ -0,0 +1,14 @@
1
+ .PHONY: format lint test
2
+
3
+ VENV = .venv
4
+ BIN = $(VENV)/bin
5
+
6
+ format:
7
+ $(BIN)/ruff format src/ tests/
8
+ $(BIN)/ruff check --fix src/ tests/
9
+
10
+ lint:
11
+ $(BIN)/ruff check src/ tests/
12
+
13
+ test:
14
+ $(BIN)/pytest tests/ -v
cpnx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,327 @@
1
+ Metadata-Version: 2.4
2
+ Name: cpnx
3
+ Version: 0.1.0
4
+ Summary: Coloured Petri Net executor for concurrent Python pipelines
5
+ Project-URL: Repository, https://github.com/philgresh/cpnx
6
+ Project-URL: Documentation, https://github.com/philgresh/cpnx#readme
7
+ Author-email: Phil Gresham <phil@gresham.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: colored-petri-net,coloured-petri-net,concurrency,cpn,orchestration,petri-net,pipeline,workflow
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.10
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0; extra == 'dev'
21
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # cpnx
25
+
26
+ [![PyPI version](https://img.shields.io/pypi/v/cpnx.svg)](https://pypi.org/project/cpnx/)
27
+ [![Python versions](https://img.shields.io/pypi/pyversions/cpnx.svg)](https://pypi.org/project/cpnx/)
28
+ [![CI status](https://github.com/philgresh/cpnx/actions/workflows/ci.yml/badge.svg)](https://github.com/philgresh/cpnx/actions)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30
+
31
+ **cpnx** is a Coloured Petri Net (CPN) executor for concurrent Python pipelines — zero dependencies, stdlib-only threading.
32
+
33
+ ---
34
+
35
+ ## Motivation
36
+
37
+ Python has excellent Petri net modeling libraries (like [SNAKES](https://snakes.ibisc.univ-evry.fr/) for formal analysis and [pm4py](https://pm4py.fit.fraunhofer.de/) for process mining) but lacks a lightweight concurrent runtime executor. Developers managing resource-constrained workflows (GPU slots, API rate limits, database connection pools) often stitch together `threading.Semaphore`, `ThreadPoolExecutor`, and `queue.Queue` by hand — ad-hoc wiring that is hard to visualise and impossible to formally reason about.
38
+
39
+ **cpnx** fills this gap: it models your concurrent pipeline as a Coloured Petri Net where transitions execute real work on thread pools, resource tokens are returned atomically on failure, and the net's structure makes resource contention a mathematical property rather than scattered locking code.
40
+
41
+ The execution model is aligned with Jensen's CPN formalism (see [Theoretical Foundation](#theoretical-foundation)), so the net you write is also amenable to formal analysis with standard CPN tools.
42
+
43
+ ---
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install cpnx
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Quickstart
54
+
55
+ A pool of 2 GPU slots shared across 10 concurrent training jobs:
56
+
57
+ ```python
58
+ """examples/gpu_pipeline.py — GPU slot management with cpnx."""
59
+
60
+ import time
61
+ from cpnx import InputArc, OutputArc, PetriNet, Place, ResourcePlace, Token, Transition
62
+
63
+
64
+ def train_model(tokens: list[Token]) -> list[Token]:
65
+ data = tokens[0]
66
+ time.sleep(0.5) # simulate GPU work
67
+ # Tokens are immutable — produce a new one with updated payload
68
+ return [data.evolve(payload_updates={"trained": True})]
69
+
70
+
71
+ net = PetriNet(max_workers=4)
72
+
73
+ net.add_place(Place("raw_data"))
74
+ net.add_place(Place("trained_models"))
75
+ net.add_place(ResourcePlace("gpu_slots", capacity=2))
76
+
77
+ net.add_transition(Transition(
78
+ name="train",
79
+ inputs=[InputArc("raw_data"), InputArc("gpu_slots")],
80
+ outputs=[OutputArc("trained_models"), OutputArc("gpu_slots")],
81
+ action=train_model,
82
+ ))
83
+
84
+ for i in range(10):
85
+ net.deposit("raw_data", Token(payload={"model_id": i}))
86
+
87
+ net.run(deadline=time.monotonic() + 30)
88
+
89
+ print(f"Trained: {len(net.places['trained_models'].tokens)}")
90
+ print(f"GPU slots returned: {len(net.places['gpu_slots'].tokens)}")
91
+ # Trained: 10
92
+ # GPU slots returned: 2
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Core Concepts
98
+
99
+ A CPN consists of **places** (token containers), **transitions** (processing steps), and **arcs** (directed connections). Tokens carry a **colour** that determines which places they may occupy and which transitions may consume them.
100
+
101
+ ```mermaid
102
+ graph LR
103
+ raw_data(("Place: raw_data")) --> train[Transition: train]
104
+ gpu_slots(("ResourcePlace: gpu_slots\n(capacity=2)")) --> train
105
+ train --> trained_models(("Place: trained_models"))
106
+ train --> gpu_slots
107
+
108
+ style raw_data fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
109
+ style trained_models fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
110
+ style gpu_slots fill:#efebe9,stroke:#5d4037,stroke-width:2px
111
+ style train fill:#fffde7,stroke:#fbc02d,stroke-width:2px
112
+ ```
113
+
114
+ ### Tokens
115
+
116
+ Tokens are **immutable**. Their `payload` is a [`FrozenDict`](#frozendict) — a hashable, recursively-immutable mapping. To produce a token with updated data, use `token.evolve()`:
117
+
118
+ ```python
119
+ result = token.evolve(payload_updates={"score": 0.92})
120
+ ```
121
+
122
+ Each token carries a `color: str | None` field — the CPN colour. `None` means an uncoloured data token; `"resource"` is the built-in colour for permit tokens. You can define your own colours for domain-typed nets.
123
+
124
+ ### Places
125
+
126
+ All places are thread-safe.
127
+
128
+ | Type | Behaviour |
129
+ |---|---|
130
+ | `Place` | Unbounded FIFO queue for data/work items |
131
+ | `ResourcePlace(capacity)` | Pre-filled bounded pool of `"resource"` permit tokens; returned on transition completion or failure |
132
+ | `PacedResourcePlace(capacity, pacing_secs)` | Like `ResourcePlace`, but returned tokens cool down for `pacing_secs` before becoming reusable (rate-limiting) |
133
+ | `ThresholdPlace(threshold)` | Tokens only consumable once the queue depth reaches `threshold` (batch accumulation) |
134
+
135
+ ### Transitions
136
+
137
+ A transition is **enabled** when all input places contain sufficient tokens and any guard expression evaluates to `True`. When fired, it consumes input tokens, executes the action on a thread pool, and deposits output tokens.
138
+
139
+ **Resource Return Invariant:** if a transition action raises, the engine catches the exception, routes the data token to the `error_place` (default: `"failed"`), and atomically returns all resource tokens to their source places. Deadlocks from failed actions are structurally impossible.
140
+
141
+ ### Arc Expressions
142
+
143
+ Both `InputArc` and `OutputArc` accept an `expression` — a callable that filters or orders token consumption (input) or gates token deposit (output):
144
+
145
+ ```python
146
+ # Consume the highest-priority lead first
147
+ InputArc("leads", count=1,
148
+ expression=lambda tokens: sorted(tokens, key=lambda t: -t.payload.get("score", 0)))
149
+
150
+ # Only deposit to the output place if processing succeeded
151
+ OutputArc("results", expression=lambda tokens: bool(tokens))
152
+ ```
153
+
154
+ ### Marking
155
+
156
+ The **marking** is the complete distribution of tokens across all places at a given moment — the formal CPN state:
157
+
158
+ ```python
159
+ m = net.marking # dict[str, list[Token]]
160
+ dead = net.is_dead() # True if no transition can fire in this marking
161
+ quiet = net.is_quiescent() # True if dead AND no in-flight transitions
162
+ ```
163
+
164
+ ---
165
+
166
+ ## API Reference
167
+
168
+ ### `Token`
169
+
170
+ ```python
171
+ @dataclass(frozen=True)
172
+ class Token:
173
+ id: str # 16-char hex, auto-generated
174
+ payload: FrozenDict # immutable enrichment data; use .evolve() to update
175
+ created_at: float # monotonic creation timestamp
176
+ color: str | None # CPN colour; None = uncoloured, "resource" = permit token
177
+ available_at: float # timed CPN: earliest time this token may be consumed
178
+
179
+ def evolve(self, payload_updates: dict | None = None, **field_updates) -> Token: ...
180
+ @property
181
+ def is_resource(self) -> bool: ... # shorthand for color == "resource"
182
+ ```
183
+
184
+ ### `FrozenDict`
185
+
186
+ An immutable, hashable mapping. Nested dicts and lists are frozen recursively at construction time.
187
+
188
+ ```python
189
+ fd = FrozenDict({"x": 1, "tags": ["a", "b"]})
190
+ fd["x"] # 1
191
+ fd.as_dict() # {"x": 1, "tags": ["a", "b"]} — plain dict, JSON-serialisable
192
+ fd.set("y", 2) # returns a new FrozenDict — fd is unchanged
193
+ ```
194
+
195
+ ### Places
196
+
197
+ ```python
198
+ Place(name: str, bound: int | None = None, color_set: set[str] | None = None,
199
+ initial_marking: list[Token] | None = None)
200
+
201
+ ResourcePlace(name: str, capacity: int)
202
+ PacedResourcePlace(name: str, capacity: int, pacing_secs: float)
203
+ ThresholdPlace(name: str, threshold: int)
204
+ ```
205
+
206
+ - `bound` — k-bounded place; raises if a deposit would exceed capacity (standard CPN)
207
+ - `color_set` — if set, `deposit()` rejects tokens whose `color` is not in the set
208
+ - `initial_marking` — tokens deposited at construction time
209
+
210
+ ### Arcs
211
+
212
+ ```python
213
+ InputArc(place: str, count: int = 1, consume_all: bool = False,
214
+ settle_secs: float = 0.0,
215
+ expression: Callable[[list[Token]], list[Token]] | None = None)
216
+
217
+ OutputArc(place: str, count: int = 1,
218
+ expression: Callable[[list[Token]], bool] | None = None)
219
+ ```
220
+
221
+ ### `Transition`
222
+
223
+ ```python
224
+ @dataclass
225
+ class Transition:
226
+ name: str
227
+ inputs: list[InputArc]
228
+ outputs: list[OutputArc]
229
+ action: Callable[[list[Token]], list[Token]]
230
+ guard: Callable[[], bool] | str | None = None # transition guard (CPN standard)
231
+ priority: int = 10 # lower fires first among equally-enabled transitions
232
+ ```
233
+
234
+ ### `PetriNet`
235
+
236
+ ```python
237
+ class PetriNet:
238
+ def __init__(self, max_workers: int = 4, timeout_secs: float = 30.0,
239
+ expr_timeout_secs: float = 0.1, error_place: str = "failed",
240
+ places: list[Place] | None = None,
241
+ transitions: list[Transition] | None = None): ...
242
+
243
+ def add_place(self, place: Place) -> None: ...
244
+ def add_transition(self, transition: Transition) -> None: ...
245
+ def deposit(self, place_name: str, token: Token) -> None: ...
246
+
247
+ def step(self) -> bool: ... # fire one enabled transition; False if none
248
+ def run(self, deadline: float) -> None: ... # loop until quiescent or deadline
249
+
250
+ @property
251
+ def marking(self) -> dict[str, list[Token]]: ... # current CPN marking
252
+ def is_dead(self) -> bool: ... # no transition enabled in current marking
253
+ def is_quiescent(self) -> bool: ... # dead AND no in-flight transitions
254
+ def advance_time(self, t: float) -> None: ... # advance timed CPN model clock
255
+ def snapshot(self) -> dict: ... # JSON-serialisable marking snapshot
256
+ def to_dot(self) -> str: ... # Graphviz DOT representation
257
+
258
+ # Callback hooks
259
+ on_transition_fired: Callable[[str, float], None] | None # (name, duration_secs)
260
+ on_token_deposited: Callable[[str, Token], None] | None # (place_name, token)
261
+ on_error: Callable[[str, Exception, Token | None], None] | None # (name, exc, token)
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Examples
267
+
268
+ - [examples/gpu_pipeline.py](examples/gpu_pipeline.py) — GPU slot pool; shows concurrent throttling
269
+ - [examples/api_rate_limit.py](examples/api_rate_limit.py) — paced resource tokens enforce external API rate limits
270
+ - [examples/etl_pipeline.py](examples/etl_pipeline.py) — multi-stage ETL using `ThresholdPlace` for batch accumulation
271
+
272
+ ---
273
+
274
+ ## Sandboxing & Pure Evaluation
275
+
276
+ cpnx supports two forms of guard and arc expressions:
277
+
278
+ 1. **String expressions** — evaluated by `SandboxEvaluator` via static AST analysis against a strict allowlist of mathematical and comparison operations. Fully hermetic.
279
+
280
+ 2. **Callable expressions** — Python functions or lambdas. Executed in a separate thread pool (`cpnx-expr`) bounded by `expr_timeout_secs` (default 100 ms). Not I/O-isolated, but `verify_callable_purity` performs AST analysis at construction time to block common I/O calls (`open`, `print`, `time.sleep`, `os.system`, etc.). Full hermetic isolation requires string expressions.
281
+
282
+ ---
283
+
284
+ ## FAQ
285
+
286
+ ### Why not Airflow or Celery?
287
+
288
+ Airflow and Celery are excellent for distributed, long-running DAGs. They require external brokers (Redis, Postgres) and add deployment complexity. cpnx is an in-process threading library for fine-grained resource control within a single Python process — no infrastructure required.
289
+
290
+ ### Why not asyncio?
291
+
292
+ ML/AI pipelines, CPU-bound parsing, and legacy database integrations use synchronous libraries. Thread pools let synchronous code run concurrently without rewriting blocking calls to async.
293
+
294
+ ### Can it prevent deadlocks?
295
+
296
+ Structurally, yes — as long as resource tokens are always returned (which the Resource Return Invariant enforces). Beyond that, CPNs are amenable to formal reachability analysis: expressing constraints as explicit token structures rather than scattered locks makes deadlock-freedom properties checkable with standard CPN tools.
297
+
298
+ ---
299
+
300
+ ## Theoretical Foundation
301
+
302
+ cpnx's execution model is aligned with **Coloured Petri Nets (CPNs)** as formalised by Kurt Jensen's group at Aarhus University. The key CPN concepts — colour sets, arc expressions, transition guards, formal markings, and k-bounded places — map directly onto cpnx's API.
303
+
304
+ **References:**
305
+
306
+ - Jensen, K. et al. — *CPN Group at Aarhus University* — https://cs.au.dk/cpnets
307
+ The canonical reference for CPN theory, tools (CPN Tools), and formalism.
308
+
309
+ - Winkler, T. et al. — *CPN-Py: A Python Framework for Coloured Petri Nets* (2025) — https://arxiv.org/html/2506.12238v1
310
+ The closest Python CPN library; cpnx differs by targeting concurrent **execution** rather than sequential **simulation** and formal state-space analysis.
311
+
312
+ **Where cpnx intentionally diverges from standard CPN theory:**
313
+
314
+ | cpnx feature | Status |
315
+ |---|---|
316
+ | `PacedResourcePlace`, `settle_secs` | Pragmatic concurrency extensions; no CPN equivalent |
317
+ | `expr_timeout_secs`, `verify_callable_purity` | Pragmatic sandboxing; no CPN equivalent |
318
+ | `is_quiescent()` | Dead marking AND no in-flight threads; no single CPN term |
319
+ | `ResourcePlace`, `ThresholdPlace` | CPN patterns expressed as typed place shorthands |
320
+ | `Place.bound` | Standard CPN: k-bounded place |
321
+ | `Token.color`, `Place.color_set` | Standard CPN: colours and colour sets |
322
+
323
+ ---
324
+
325
+ ## License
326
+
327
+ MIT — see [LICENSE](LICENSE).