wavekit 0.5.5__tar.gz → 0.6.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 (28) hide show
  1. {wavekit-0.5.5 → wavekit-0.6.0}/PKG-INFO +66 -10
  2. {wavekit-0.5.5 → wavekit-0.6.0}/README.md +64 -9
  3. wavekit-0.6.0/build.py +38 -0
  4. {wavekit-0.5.5 → wavekit-0.6.0}/pyproject.toml +2 -1
  5. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/__init__.py +27 -24
  6. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/pattern/__init__.py +2 -1
  7. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/pattern/dsl.py +84 -20
  8. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/pattern/engine.py +68 -30
  9. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/pattern/result.py +1 -1
  10. wavekit-0.6.0/src/wavekit/pattern/steps.py +216 -0
  11. wavekit-0.6.0/src/wavekit/readers/fsdb/npi_fsdb.pxd +82 -0
  12. wavekit-0.6.0/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +739 -0
  13. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/readers/fsdb/reader.py +44 -17
  14. wavekit-0.6.0/src/wavekit/readers/fst/reader.py +261 -0
  15. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/readers/vcd/reader.py +18 -0
  16. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/waveform.py +163 -1
  17. wavekit-0.5.5/build.py +0 -52
  18. wavekit-0.5.5/src/wavekit/pattern/steps.py +0 -142
  19. wavekit-0.5.5/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -122
  20. wavekit-0.5.5/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +0 -328
  21. {wavekit-0.5.5 → wavekit-0.6.0}/LICENSE +0 -0
  22. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/pattern/instance.py +0 -0
  23. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/readers/base.py +0 -0
  24. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/readers/expr_parser.py +0 -0
  25. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/readers/pattern_parser.py +0 -0
  26. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/readers/value_change.pyx +0 -0
  27. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/scope.py +0 -0
  28. {wavekit-0.5.5 → wavekit-0.6.0}/src/wavekit/signal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavekit
3
- Version: 0.5.5
3
+ Version: 0.6.0
4
4
  Summary: a fundamental package for digital circuit waveform analysis
5
5
  License-File: LICENSE
6
6
  Author: cxzzzz
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Requires-Dist: numpy (>=2.0.0,<3.0.0)
17
+ Requires-Dist: pylibfst (>=0.2.1,<0.3.0)
17
18
  Requires-Dist: pytest (>=8.2.2,<9.0.0)
18
19
  Requires-Dist: vcdvcd (>=2.3.5,<3.0.0)
19
20
  Project-URL: Repository, https://github.com/cxzzzz/wavekit
@@ -24,15 +25,21 @@ Description-Content-Type: text/markdown
24
25
  [![CI](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml/badge.svg)](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
25
26
  [![PyPI version](https://img.shields.io/pypi/v/wavekit.svg)](https://pypi.org/project/wavekit/)
26
27
  [![Python Versions](https://img.shields.io/pypi/pyversions/wavekit.svg)](https://pypi.org/project/wavekit/)
28
+ [![Downloads](https://img.shields.io/pypi/dm/wavekit.svg)](https://pypi.org/project/wavekit/)
27
29
  [![License](https://img.shields.io/github/license/cxzzzz/wavekit.svg)](LICENSE)
28
30
 
29
- **Wavekit** is a fundamental Python library for digital waveform analysis. By seamlessly converting VCD and FSDB data into Numpy arrays, it empowers engineers to perform high-performance signal processing, protocol analysis, and automated verification with ease.
31
+ English | [中文](README_ZH.md)
32
+
33
+ **Wavekit** is a fundamental Python library for digital waveform analysis. By seamlessly converting VCD, FST, and FSDB data into Numpy arrays, it empowers engineers to perform high-performance signal processing, protocol analysis, and automated verification with ease.
34
+
35
+ > 🤖 **AI Integration**: [wavekit-mcp](https://github.com/cxzzzz/wavekit-mcp) — MCP server for AI-assisted waveform analysis. Let AI load signals and run pattern matching — no manual coding required.
30
36
 
31
37
  ## ✨ Features
32
38
 
33
- - **High Performance & Easy Loading**: Cython-optimized VCD/FSDB parsers with Numpy-backed storage for speed and memory efficiency, plus flexible batch signal extraction via brace expansion, integer ranges, and regular expressions.
39
+ - **Flexible Signal Extraction**: Flexible batch signal extraction via brace expansion, integer ranges, and regular expressions — load groups of related signals in one call.
34
40
  - **Rich Analysis Tools**: Numpy-like API for arithmetic, masking, bit-field manipulation, edge detection, and time/cycle slicing — compose complex signal queries in just a few lines.
35
41
  - **Pattern Matching**: NFA-based temporal pattern engine that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
42
+ - **High-Performance Parsing & Storage**: VCD, FST, and FSDB readers with Numpy-backed storage for fast loading and memory efficiency, handling large simulation files with ease.
36
43
 
37
44
  ## 📦 Installation
38
45
 
@@ -40,11 +47,14 @@ Description-Content-Type: text/markdown
40
47
  pip install wavekit
41
48
  ```
42
49
 
43
- **Note**: To read FSDB files, ensure the `VERDI_HOME` environment variable is set before installation.
50
+ **Note**: To read FSDB files, the Verdi runtime library (`libNPI.so`) must be available at runtime. Configure via:
51
+ - `WAVEKIT_NPI_LIB` — direct path to `libNPI.so`
52
+ - `VERDI_HOME` — Verdi installation directory (searches `$VERDI_HOME/share/NPI/lib/...`)
53
+ - `LD_LIBRARY_PATH` — system library search path
44
54
 
45
55
  ## 🚀 Quick Start
46
56
 
47
- > The examples below use placeholder filenames such as `sim.vcd`. Replace them with the path to your own VCD or FSDB file, and adjust signal paths to match your design hierarchy.
57
+ > The examples below use placeholder filenames such as `sim.vcd`. Replace them with the path to your own VCD, FST, or FSDB file, and adjust signal paths to match your design hierarchy.
48
58
 
49
59
  ### 1. Batch Signal Extraction
50
60
 
@@ -157,7 +167,7 @@ print(f"Read data: {valid.captures['rdata'].value}")
157
167
  **AXI Write Burst (multi-beat)**
158
168
 
159
169
  ```python
160
- beat = Pattern().wait(wvalid & wready).capture("beats[]", wdata)
170
+ beat = Pattern().wait(wvalid & wready).capture("beats", wdata, mode="list")
161
171
 
162
172
  result = (
163
173
  Pattern()
@@ -195,7 +205,7 @@ print(f"Stall durations: {stalls.duration.value} cycles")
195
205
 
196
206
  | Method | Description |
197
207
  |--------|-------------|
198
- | `VcdReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires `VERDI_HOME`. |
208
+ | `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`). |
199
209
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
200
210
  | `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
201
211
  | `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
@@ -258,15 +268,33 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
258
268
  | `wave.rising_edge()` | True at 0→1 transitions |
259
269
  | `wave.falling_edge()` | True at 1→0 transitions |
260
270
 
271
+ **Relative time access**
272
+
273
+ | Method | Description |
274
+ |--------|-------------|
275
+ | `wave.relative(offset, pad, pad_value)` | Shift by *offset* cycles (positive = future, negative = past) |
276
+ | `wave.ahead(n, pad, pad_value)` | Look *n* cycles into the future (shorthand for `relative(n)`) |
277
+ | `wave.back(n, pad, pad_value)` | Look *n* cycles into the past (shorthand for `relative(-n)`) |
278
+
279
+ `pad` controls boundary handling: `'repeat'` (default) pads with the first/last value, `'value'` pads with a given `pad_value`.
280
+
281
+ ```python
282
+ # Rising edge detection
283
+ rising = (wave == 0) & wave.ahead()
284
+
285
+ # Compare current vs 3 cycles ago
286
+ changed = wave != wave.back(3)
287
+ ```
288
+
261
289
  ---
262
290
 
263
291
  ### Pattern
264
292
 
265
293
  | Method | Description |
266
294
  |--------|-------------|
267
- | `.wait(cond, guard=None, channel=None)` | Block until `cond` is True. `guard` is checked each waiting cycle. `channel` enforces FIFO ordering among concurrent instances. |
268
- | `.delay(n, guard=None)` | Advance `n` cycles. `delay(0)` is a no-op. |
269
- | `.capture(name, signal)` | Record signal value at current cycle. `name[]` appends to a list. |
295
+ | `.wait(cond, *, require=None, channel=None, tick=True)` | Block until `cond` is True. `require` is checked each waiting cycle (failure → `REQUIRE_VIOLATED`). `channel` binds the wait to a shared FIFO consumer group (see [Channels](#channels) below). `tick=False` matches on the current cycle without consuming it. |
296
+ | `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
297
+ | `.capture(name, signal, *, mode='last')` | Record signal value at current cycle. `mode='last'` (default) overwrites; `'first'` keeps the first write; `'list'` appends to a list. |
270
298
  | `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
271
299
  | `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
272
300
  | `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
@@ -274,6 +302,34 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
274
302
  | `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
275
303
  | `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
276
304
 
305
+ **Channels**
306
+
307
+ A `Channel` is an identity object representing a shared FIFO consumer group: at most one in-flight pattern instance may consume per cycle. Each `wait()` step has its own implicit channel, so multiple instances of the same pattern automatically serialize one-per-cycle on that step. Pass an explicit `Channel` (or `callable(index, captures) -> Channel`) when you need to override the default serialization — typically when events arrive on physically parallel buses (multi-bank memory, multi-lane retire) and several instances should consume *concurrently* by routing each to its own per-key channel.
308
+
309
+ ```python
310
+ from collections import defaultdict
311
+ from wavekit import Channel, Pattern
312
+
313
+ # Multi-bank cache: each bank has its own response port, so two banks
314
+ # can return data in the *same* cycle. Without partitioning, the default
315
+ # serialization rule would force one instance to wait an extra cycle.
316
+ # A per-bank Channel lets each in-flight read consume from its own bank.
317
+ banks = defaultdict(Channel)
318
+
319
+ result = (
320
+ Pattern()
321
+ .wait(req_valid)
322
+ .capture('bank', req_addr & 1)
323
+ .wait(
324
+ lambda i, cap: bank_valid[cap['bank']].value[i],
325
+ channel=lambda i, cap: banks[cap['bank']],
326
+ )
327
+ .capture('rdata',
328
+ lambda i, cap: bank_data[cap['bank']].value[i])
329
+ .match()
330
+ )
331
+ ```
332
+
277
333
  **`MatchResult`**
278
334
 
279
335
  | Field | Description |
@@ -3,15 +3,21 @@
3
3
  [![CI](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml/badge.svg)](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
4
4
  [![PyPI version](https://img.shields.io/pypi/v/wavekit.svg)](https://pypi.org/project/wavekit/)
5
5
  [![Python Versions](https://img.shields.io/pypi/pyversions/wavekit.svg)](https://pypi.org/project/wavekit/)
6
+ [![Downloads](https://img.shields.io/pypi/dm/wavekit.svg)](https://pypi.org/project/wavekit/)
6
7
  [![License](https://img.shields.io/github/license/cxzzzz/wavekit.svg)](LICENSE)
7
8
 
8
- **Wavekit** is a fundamental Python library for digital waveform analysis. By seamlessly converting VCD and FSDB data into Numpy arrays, it empowers engineers to perform high-performance signal processing, protocol analysis, and automated verification with ease.
9
+ English | [中文](README_ZH.md)
10
+
11
+ **Wavekit** is a fundamental Python library for digital waveform analysis. By seamlessly converting VCD, FST, and FSDB data into Numpy arrays, it empowers engineers to perform high-performance signal processing, protocol analysis, and automated verification with ease.
12
+
13
+ > 🤖 **AI Integration**: [wavekit-mcp](https://github.com/cxzzzz/wavekit-mcp) — MCP server for AI-assisted waveform analysis. Let AI load signals and run pattern matching — no manual coding required.
9
14
 
10
15
  ## ✨ Features
11
16
 
12
- - **High Performance & Easy Loading**: Cython-optimized VCD/FSDB parsers with Numpy-backed storage for speed and memory efficiency, plus flexible batch signal extraction via brace expansion, integer ranges, and regular expressions.
17
+ - **Flexible Signal Extraction**: Flexible batch signal extraction via brace expansion, integer ranges, and regular expressions — load groups of related signals in one call.
13
18
  - **Rich Analysis Tools**: Numpy-like API for arithmetic, masking, bit-field manipulation, edge detection, and time/cycle slicing — compose complex signal queries in just a few lines.
14
19
  - **Pattern Matching**: NFA-based temporal pattern engine that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
20
+ - **High-Performance Parsing & Storage**: VCD, FST, and FSDB readers with Numpy-backed storage for fast loading and memory efficiency, handling large simulation files with ease.
15
21
 
16
22
  ## 📦 Installation
17
23
 
@@ -19,11 +25,14 @@
19
25
  pip install wavekit
20
26
  ```
21
27
 
22
- **Note**: To read FSDB files, ensure the `VERDI_HOME` environment variable is set before installation.
28
+ **Note**: To read FSDB files, the Verdi runtime library (`libNPI.so`) must be available at runtime. Configure via:
29
+ - `WAVEKIT_NPI_LIB` — direct path to `libNPI.so`
30
+ - `VERDI_HOME` — Verdi installation directory (searches `$VERDI_HOME/share/NPI/lib/...`)
31
+ - `LD_LIBRARY_PATH` — system library search path
23
32
 
24
33
  ## 🚀 Quick Start
25
34
 
26
- > The examples below use placeholder filenames such as `sim.vcd`. Replace them with the path to your own VCD or FSDB file, and adjust signal paths to match your design hierarchy.
35
+ > The examples below use placeholder filenames such as `sim.vcd`. Replace them with the path to your own VCD, FST, or FSDB file, and adjust signal paths to match your design hierarchy.
27
36
 
28
37
  ### 1. Batch Signal Extraction
29
38
 
@@ -136,7 +145,7 @@ print(f"Read data: {valid.captures['rdata'].value}")
136
145
  **AXI Write Burst (multi-beat)**
137
146
 
138
147
  ```python
139
- beat = Pattern().wait(wvalid & wready).capture("beats[]", wdata)
148
+ beat = Pattern().wait(wvalid & wready).capture("beats", wdata, mode="list")
140
149
 
141
150
  result = (
142
151
  Pattern()
@@ -174,7 +183,7 @@ print(f"Stall durations: {stalls.duration.value} cycles")
174
183
 
175
184
  | Method | Description |
176
185
  |--------|-------------|
177
- | `VcdReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires `VERDI_HOME`. |
186
+ | `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`). |
178
187
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
179
188
  | `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
180
189
  | `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
@@ -237,15 +246,33 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
237
246
  | `wave.rising_edge()` | True at 0→1 transitions |
238
247
  | `wave.falling_edge()` | True at 1→0 transitions |
239
248
 
249
+ **Relative time access**
250
+
251
+ | Method | Description |
252
+ |--------|-------------|
253
+ | `wave.relative(offset, pad, pad_value)` | Shift by *offset* cycles (positive = future, negative = past) |
254
+ | `wave.ahead(n, pad, pad_value)` | Look *n* cycles into the future (shorthand for `relative(n)`) |
255
+ | `wave.back(n, pad, pad_value)` | Look *n* cycles into the past (shorthand for `relative(-n)`) |
256
+
257
+ `pad` controls boundary handling: `'repeat'` (default) pads with the first/last value, `'value'` pads with a given `pad_value`.
258
+
259
+ ```python
260
+ # Rising edge detection
261
+ rising = (wave == 0) & wave.ahead()
262
+
263
+ # Compare current vs 3 cycles ago
264
+ changed = wave != wave.back(3)
265
+ ```
266
+
240
267
  ---
241
268
 
242
269
  ### Pattern
243
270
 
244
271
  | Method | Description |
245
272
  |--------|-------------|
246
- | `.wait(cond, guard=None, channel=None)` | Block until `cond` is True. `guard` is checked each waiting cycle. `channel` enforces FIFO ordering among concurrent instances. |
247
- | `.delay(n, guard=None)` | Advance `n` cycles. `delay(0)` is a no-op. |
248
- | `.capture(name, signal)` | Record signal value at current cycle. `name[]` appends to a list. |
273
+ | `.wait(cond, *, require=None, channel=None, tick=True)` | Block until `cond` is True. `require` is checked each waiting cycle (failure → `REQUIRE_VIOLATED`). `channel` binds the wait to a shared FIFO consumer group (see [Channels](#channels) below). `tick=False` matches on the current cycle without consuming it. |
274
+ | `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
275
+ | `.capture(name, signal, *, mode='last')` | Record signal value at current cycle. `mode='last'` (default) overwrites; `'first'` keeps the first write; `'list'` appends to a list. |
249
276
  | `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
250
277
  | `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
251
278
  | `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
@@ -253,6 +280,34 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
253
280
  | `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
254
281
  | `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
255
282
 
283
+ **Channels**
284
+
285
+ A `Channel` is an identity object representing a shared FIFO consumer group: at most one in-flight pattern instance may consume per cycle. Each `wait()` step has its own implicit channel, so multiple instances of the same pattern automatically serialize one-per-cycle on that step. Pass an explicit `Channel` (or `callable(index, captures) -> Channel`) when you need to override the default serialization — typically when events arrive on physically parallel buses (multi-bank memory, multi-lane retire) and several instances should consume *concurrently* by routing each to its own per-key channel.
286
+
287
+ ```python
288
+ from collections import defaultdict
289
+ from wavekit import Channel, Pattern
290
+
291
+ # Multi-bank cache: each bank has its own response port, so two banks
292
+ # can return data in the *same* cycle. Without partitioning, the default
293
+ # serialization rule would force one instance to wait an extra cycle.
294
+ # A per-bank Channel lets each in-flight read consume from its own bank.
295
+ banks = defaultdict(Channel)
296
+
297
+ result = (
298
+ Pattern()
299
+ .wait(req_valid)
300
+ .capture('bank', req_addr & 1)
301
+ .wait(
302
+ lambda i, cap: bank_valid[cap['bank']].value[i],
303
+ channel=lambda i, cap: banks[cap['bank']],
304
+ )
305
+ .capture('rdata',
306
+ lambda i, cap: bank_data[cap['bank']].value[i])
307
+ .match()
308
+ )
309
+ ```
310
+
256
311
  **`MatchResult`**
257
312
 
258
313
  | Field | Description |
wavekit-0.6.0/build.py ADDED
@@ -0,0 +1,38 @@
1
+ import numpy as np
2
+ from Cython.Build import cythonize
3
+ from setuptools import Extension, setup
4
+
5
+ extensions = [
6
+ Extension(
7
+ 'src.wavekit.readers.value_change',
8
+ sources=['src/wavekit/readers/value_change.pyx'],
9
+ include_dirs=[np.get_include()],
10
+ extra_compile_args=['-fpic', '-O3', '-march=native'],
11
+ extra_link_args=['-O3', '-march=native'],
12
+ language='c++',
13
+ ),
14
+ Extension(
15
+ 'src.wavekit.readers.fsdb.npi_fsdb_reader',
16
+ sources=['src/wavekit/readers/fsdb/npi_fsdb_reader.pyx'],
17
+ include_dirs=[np.get_include()],
18
+ libraries=['dl'],
19
+ extra_compile_args=['-fpic', '-O3', '-march=native'],
20
+ extra_link_args=['-O3', '-march=native'],
21
+ language='c++',
22
+ ),
23
+ ]
24
+
25
+ setup(
26
+ ext_modules=cythonize(
27
+ extensions,
28
+ compiler_directives={
29
+ 'language_level': 3,
30
+ 'embedsignature': True,
31
+ 'boundscheck': False,
32
+ 'wraparound': False,
33
+ 'cdivision': True,
34
+ 'nonecheck': False,
35
+ },
36
+ ),
37
+ script_args=['build_ext', '--inplace'],
38
+ )
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wavekit"
3
- version = "0.5.5"
3
+ version = "0.6.0"
4
4
  description = "a fundamental package for digital circuit waveform analysis"
5
5
  authors = ["cxzzzz <cxz19961010@outlook.com>"]
6
6
  readme = "README.md"
@@ -12,6 +12,7 @@ include = ["src/wavekit/**/*.so"]
12
12
  python = "^3.9"
13
13
  numpy = "^2.0.0"
14
14
  vcdvcd = "^2.3.5"
15
+ pylibfst = "^0.2.1"
15
16
  pytest = "^8.2.2"
16
17
 
17
18
  [tool.poetry.group.dev.dependencies]
@@ -13,10 +13,12 @@ try:
13
13
  except metadata.PackageNotFoundError:
14
14
  __version__ = 'unknown'
15
15
 
16
+ from .pattern import Channel as Channel
16
17
  from .pattern import MatchResult as MatchResult
17
18
  from .pattern import MatchStatus as MatchStatus
18
19
  from .pattern import Pattern as Pattern
19
20
  from .pattern import PatternError as PatternError
21
+ from .readers.fst.reader import FstReader as FstReader
20
22
  from .readers.vcd.reader import VcdReader as VcdReader
21
23
  from .scope import Scope as Scope
22
24
  from .signal import Signal as Signal
@@ -27,6 +29,7 @@ __all__ = [
27
29
  'Waveform',
28
30
  'VcdReader',
29
31
  'FsdbReader',
32
+ 'FstReader',
30
33
  'Scope',
31
34
  'Signal',
32
35
  'SignalCompositeType',
@@ -34,35 +37,35 @@ __all__ = [
34
37
  'MatchResult',
35
38
  'MatchStatus',
36
39
  'PatternError',
40
+ 'Channel',
37
41
  'has_fsdb_support',
38
42
  ]
39
43
 
40
- # Detect if FSDB support is available (compiled with VERDI_HOME)
41
- import importlib.util
42
-
43
- _fsdb_available = (
44
- importlib.util.find_spec('.readers.fsdb.npi_fsdb_reader', package=__name__) is not None
45
- )
46
-
47
-
48
- def has_fsdb_support() -> bool:
49
- """Check if FsdbReader is available in the current installation."""
50
- return _fsdb_available
51
-
44
+ try:
45
+ from .readers.fsdb.npi_fsdb_reader import fsdb_runtime_available as _fsdb_runtime_available
46
+ from .readers.fsdb.reader import FsdbReader as FsdbReader
47
+ except Exception as _fsdb_import_error:
48
+ _fsdb_available = False
52
49
 
53
- class _FsdbReaderStub:
54
- """Placeholder that raises an error when FsdbReader is used without Verdi support."""
50
+ def has_fsdb_support() -> bool:
51
+ """Check whether the Verdi FSDB runtime is available."""
52
+ return False
55
53
 
56
- def __init__(self, *args, **kwargs):
57
- raise RuntimeError(
58
- 'FsdbReader requires Verdi to be installed and configured.\n\n'
59
- '1. Set VERDI_HOME environment variable to your Verdi installation path.\n'
60
- '2. Reinstall wavekit:\n'
61
- ' pip install --force-reinstall --no-cache-dir --no-deps wavekit'
62
- )
54
+ class _FsdbReaderStub:
55
+ """Placeholder that raises an error when the Verdi FSDB runtime is unavailable."""
63
56
 
57
+ def __init__(self, *args, **kwargs):
58
+ raise RuntimeError(
59
+ 'FsdbReader requires the Verdi FSDB runtime (libNPI.so).\n\n'
60
+ 'Set WAVEKIT_NPI_LIB to the library path, set VERDI_HOME to the Verdi '
61
+ 'installation directory, or ensure libNPI.so is in LD_LIBRARY_PATH.\n\n'
62
+ f'Import error: {_fsdb_import_error}' # noqa: F821
63
+ )
64
64
 
65
- if _fsdb_available:
66
- from .readers.fsdb.reader import FsdbReader as FsdbReader
67
- else:
68
65
  FsdbReader = _FsdbReaderStub # type: ignore[assignment]
66
+ else:
67
+ _fsdb_available = True
68
+
69
+ def has_fsdb_support() -> bool:
70
+ """Check whether the Verdi FSDB runtime is available right now."""
71
+ return _fsdb_runtime_available()
@@ -4,5 +4,6 @@ from .dsl import Pattern as Pattern
4
4
  from .engine import PatternError as PatternError
5
5
  from .result import MatchResult as MatchResult
6
6
  from .result import MatchStatus as MatchStatus
7
+ from .steps import Channel as Channel
7
8
 
8
- __all__ = ['Pattern', 'MatchResult', 'MatchStatus', 'PatternError']
9
+ __all__ = ['Pattern', 'MatchResult', 'MatchStatus', 'PatternError', 'Channel']
@@ -3,8 +3,10 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from .steps import (
6
+ _VALID_CAPTURE_MODES,
6
7
  BranchStep,
7
8
  CaptureStep,
9
+ ChannelValue,
8
10
  DelayStep,
9
11
  LoopStep,
10
12
  RepeatStep,
@@ -15,7 +17,7 @@ from .steps import (
15
17
 
16
18
  if TYPE_CHECKING:
17
19
  from .result import MatchResult
18
- from .steps import Condition, IntValue, SignalValue
20
+ from .steps import CaptureMode, Condition, IntValue, SignalValue
19
21
 
20
22
 
21
23
  class Pattern:
@@ -43,25 +45,73 @@ class Pattern:
43
45
  def wait(
44
46
  self,
45
47
  cond: Condition,
46
- guard: Condition | None = None,
47
- channel: str | None = None,
48
+ *,
49
+ require: Condition | None = None,
50
+ channel: ChannelValue | None = None,
51
+ tick: bool = True,
48
52
  ) -> Pattern:
49
53
  """Block until *cond* becomes True.
50
54
 
55
+ By default each ``wait()`` step has its own private FIFO consumer
56
+ group: when multiple in-flight instances reach this step, only one
57
+ (the oldest) advances per cycle. Pass an explicit :class:`Channel`
58
+ via ``channel=`` to share the consumer group across multiple wait
59
+ steps (e.g., for AXI request/response pairing).
60
+
51
61
  Parameters
52
62
  ----------
53
63
  cond:
54
64
  Waveform (static) or ``callable(index, captures) -> bool`` (dynamic).
55
- guard:
56
- Must remain True every cycle while waiting; violation terminates
57
- the instance with ``REQUIRE_VIOLATED``.
65
+ require:
66
+ Optional condition that must hold every cycle while waiting.
67
+ Violation terminates the instance with ``REQUIRE_VIOLATED``.
58
68
  channel:
59
- Named channel for FIFO event-consumption (see channel docs).
69
+ Explicit :class:`Channel` (or ``callable(index, captures) -> Channel``
70
+ for dynamic key-based partitioning) that binds this wait to a
71
+ shared FIFO consumer group. ``None`` (default) uses an implicit
72
+ per-step channel.
73
+ tick:
74
+ When ``True`` (default), a successful match advances time by one
75
+ cycle before the next step is evaluated. When ``False`` the next
76
+ step evaluates on the **same** cycle (zero-cycle wait), useful
77
+ for measuring intervals like ``valid → valid & ready``.
78
+
79
+ Examples
80
+ --------
81
+ Static channel shared between request and response steps::
82
+
83
+ rsp_chan = Channel()
84
+ (Pattern()
85
+ .wait(req)
86
+ .wait(rsp, channel=rsp_chan))
87
+
88
+ Dynamic per-ID partitioning for AXI out-of-order reads::
89
+
90
+ from collections import defaultdict
91
+ chans = defaultdict(Channel)
92
+ (Pattern()
93
+ .wait(arvalid & arready)
94
+ .capture('arid', arid)
95
+ .wait(
96
+ lambda i, cap: bool((rvalid & rready)[i]) and rid[i] == cap['arid'],
97
+ channel=lambda i, cap: chans[cap['arid']],
98
+ ))
99
+
100
+ Zero-cycle wait::
101
+
102
+ (Pattern()
103
+ .wait(valid)
104
+ .wait(valid & ready, tick=False)) # measures latency in same-cycle case
60
105
  """
61
- self._steps.append(WaitStep(cond=cond, guard=guard, channel=channel))
106
+ self._steps.append(WaitStep(cond=cond, require=require, channel=channel, tick=tick))
62
107
  return self
63
108
 
64
- def delay(self, n: IntValue, guard: Condition | None = None) -> Pattern:
109
+ def delay(
110
+ self,
111
+ n: IntValue,
112
+ *,
113
+ require: Condition | None = None,
114
+ ) -> Pattern:
65
115
  """Block for exactly *n* cycles.
66
116
 
67
117
  Parameters
@@ -69,25 +119,43 @@ class Pattern:
69
119
  n:
70
120
  Static ``int`` (>= 0) or ``callable(index, captures) -> int``.
71
121
  ``delay(0)`` is an epsilon step (no cycle consumed).
72
- guard:
73
- Must remain True during the delay; violation terminates
74
- the instance with ``REQUIRE_VIOLATED``.
122
+ require:
123
+ Optional condition that must hold every cycle during the delay.
124
+ Violation terminates the instance with ``REQUIRE_VIOLATED``.
75
125
  """
76
126
  if isinstance(n, int) and n < 0:
77
127
  raise ValueError(f'delay() requires n >= 0, got {n}')
78
- self._steps.append(DelayStep(n=n, guard=guard))
128
+ self._steps.append(DelayStep(n=n, require=require))
79
129
  return self
80
130
 
81
131
  # ------------------------------------------------------------------
82
132
  # Epsilon steps
83
133
  # ------------------------------------------------------------------
84
134
 
85
- def capture(self, name: str, signal: SignalValue) -> Pattern:
135
+ def capture(
136
+ self,
137
+ name: str,
138
+ signal: SignalValue,
139
+ *,
140
+ mode: CaptureMode = 'last',
141
+ ) -> Pattern:
86
142
  """Record a signal value at the current cycle.
87
143
 
88
- Use ``name[]`` to append to a list (for use inside loop/repeat).
144
+ Parameters
145
+ ----------
146
+ name:
147
+ Capture key.
148
+ signal:
149
+ Waveform (static) or ``callable(index, captures) -> Any``.
150
+ mode:
151
+ * ``'last'`` (default) – ``cap[name]`` holds the most recent value
152
+ * ``'first'`` – ``cap[name]`` holds the first value; subsequent
153
+ writes are ignored
154
+ * ``'list'`` – ``cap[name]`` is a list, each write appends
89
155
  """
90
- self._steps.append(CaptureStep(name=name, signal=signal))
156
+ if mode not in _VALID_CAPTURE_MODES:
157
+ raise ValueError(f'capture mode must be one of {_VALID_CAPTURE_MODES}, got {mode!r}')
158
+ self._steps.append(CaptureStep(name=name, signal=signal, mode=mode))
91
159
  return self
92
160
 
93
161
  def require(self, cond: Condition) -> Pattern:
@@ -166,10 +234,6 @@ class Pattern:
166
234
  ) -> MatchResult:
167
235
  """Run the pattern engine and return a :class:`MatchResult`.
168
236
 
169
- If the first step is a ``wait``, its condition is used as the trigger
170
- (only fork instances when the condition is True). Otherwise, an
171
- instance is forked on every cycle.
172
-
173
237
  Parameters
174
238
  ----------
175
239
  start_cycle: