wavekit 0.5.6__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.
- {wavekit-0.5.6 → wavekit-0.6.0}/PKG-INFO +62 -9
- {wavekit-0.5.6 → wavekit-0.6.0}/README.md +60 -8
- {wavekit-0.5.6 → wavekit-0.6.0}/pyproject.toml +2 -1
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/__init__.py +4 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/pattern/__init__.py +2 -1
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/pattern/dsl.py +84 -20
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/pattern/engine.py +68 -30
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/pattern/result.py +1 -1
- wavekit-0.6.0/src/wavekit/pattern/steps.py +216 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +51 -39
- wavekit-0.6.0/src/wavekit/readers/fst/reader.py +261 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/readers/vcd/reader.py +18 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/waveform.py +163 -1
- wavekit-0.5.6/src/wavekit/pattern/steps.py +0 -142
- {wavekit-0.5.6 → wavekit-0.6.0}/LICENSE +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/build.py +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/pattern/instance.py +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/readers/base.py +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/readers/expr_parser.py +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/readers/fsdb/reader.py +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/readers/pattern_parser.py +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/readers/value_change.pyx +0 -0
- {wavekit-0.5.6 → wavekit-0.6.0}/src/wavekit/scope.py +0 -0
- {wavekit-0.5.6 → 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.
|
|
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
|
[](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
|
|
25
26
|
[](https://pypi.org/project/wavekit/)
|
|
26
27
|
[](https://pypi.org/project/wavekit/)
|
|
28
|
+
[](https://pypi.org/project/wavekit/)
|
|
27
29
|
[](LICENSE)
|
|
28
30
|
|
|
29
|
-
|
|
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
|
-
- **
|
|
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
|
|
|
@@ -47,7 +54,7 @@ pip install wavekit
|
|
|
47
54
|
|
|
48
55
|
## 🚀 Quick Start
|
|
49
56
|
|
|
50
|
-
> 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.
|
|
51
58
|
|
|
52
59
|
### 1. Batch Signal Extraction
|
|
53
60
|
|
|
@@ -160,7 +167,7 @@ print(f"Read data: {valid.captures['rdata'].value}")
|
|
|
160
167
|
**AXI Write Burst (multi-beat)**
|
|
161
168
|
|
|
162
169
|
```python
|
|
163
|
-
beat = Pattern().wait(wvalid & wready).capture("beats
|
|
170
|
+
beat = Pattern().wait(wvalid & wready).capture("beats", wdata, mode="list")
|
|
164
171
|
|
|
165
172
|
result = (
|
|
166
173
|
Pattern()
|
|
@@ -198,7 +205,7 @@ print(f"Stall durations: {stalls.duration.value} cycles")
|
|
|
198
205
|
|
|
199
206
|
| Method | Description |
|
|
200
207
|
|--------|-------------|
|
|
201
|
-
| `VcdReader(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`). |
|
|
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`). |
|
|
202
209
|
| `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
|
|
203
210
|
| `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
|
|
204
211
|
| `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
|
|
@@ -261,15 +268,33 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
|
|
|
261
268
|
| `wave.rising_edge()` | True at 0→1 transitions |
|
|
262
269
|
| `wave.falling_edge()` | True at 1→0 transitions |
|
|
263
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
|
+
|
|
264
289
|
---
|
|
265
290
|
|
|
266
291
|
### Pattern
|
|
267
292
|
|
|
268
293
|
| Method | Description |
|
|
269
294
|
|--------|-------------|
|
|
270
|
-
| `.wait(cond,
|
|
271
|
-
| `.delay(n,
|
|
272
|
-
| `.capture(name, signal)` | Record signal value at current cycle. `
|
|
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. |
|
|
273
298
|
| `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
|
|
274
299
|
| `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
|
|
275
300
|
| `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
|
|
@@ -277,6 +302,34 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
|
|
|
277
302
|
| `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
|
|
278
303
|
| `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
|
|
279
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
|
+
|
|
280
333
|
**`MatchResult`**
|
|
281
334
|
|
|
282
335
|
| Field | Description |
|
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
[](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
|
|
4
4
|
[](https://pypi.org/project/wavekit/)
|
|
5
5
|
[](https://pypi.org/project/wavekit/)
|
|
6
|
+
[](https://pypi.org/project/wavekit/)
|
|
6
7
|
[](LICENSE)
|
|
7
8
|
|
|
8
|
-
|
|
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
|
-
- **
|
|
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
|
|
|
@@ -26,7 +32,7 @@ pip install wavekit
|
|
|
26
32
|
|
|
27
33
|
## 🚀 Quick Start
|
|
28
34
|
|
|
29
|
-
> 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.
|
|
30
36
|
|
|
31
37
|
### 1. Batch Signal Extraction
|
|
32
38
|
|
|
@@ -139,7 +145,7 @@ print(f"Read data: {valid.captures['rdata'].value}")
|
|
|
139
145
|
**AXI Write Burst (multi-beat)**
|
|
140
146
|
|
|
141
147
|
```python
|
|
142
|
-
beat = Pattern().wait(wvalid & wready).capture("beats
|
|
148
|
+
beat = Pattern().wait(wvalid & wready).capture("beats", wdata, mode="list")
|
|
143
149
|
|
|
144
150
|
result = (
|
|
145
151
|
Pattern()
|
|
@@ -177,7 +183,7 @@ print(f"Stall durations: {stalls.duration.value} cycles")
|
|
|
177
183
|
|
|
178
184
|
| Method | Description |
|
|
179
185
|
|--------|-------------|
|
|
180
|
-
| `VcdReader(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`). |
|
|
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`). |
|
|
181
187
|
| `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
|
|
182
188
|
| `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
|
|
183
189
|
| `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
|
|
@@ -240,15 +246,33 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
|
|
|
240
246
|
| `wave.rising_edge()` | True at 0→1 transitions |
|
|
241
247
|
| `wave.falling_edge()` | True at 1→0 transitions |
|
|
242
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
|
+
|
|
243
267
|
---
|
|
244
268
|
|
|
245
269
|
### Pattern
|
|
246
270
|
|
|
247
271
|
| Method | Description |
|
|
248
272
|
|--------|-------------|
|
|
249
|
-
| `.wait(cond,
|
|
250
|
-
| `.delay(n,
|
|
251
|
-
| `.capture(name, signal)` | Record signal value at current cycle. `
|
|
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. |
|
|
252
276
|
| `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
|
|
253
277
|
| `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
|
|
254
278
|
| `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
|
|
@@ -256,6 +280,34 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
|
|
|
256
280
|
| `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
|
|
257
281
|
| `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
|
|
258
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
|
+
|
|
259
311
|
**`MatchResult`**
|
|
260
312
|
|
|
261
313
|
| Field | Description |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "wavekit"
|
|
3
|
-
version = "0.
|
|
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,6 +37,7 @@ __all__ = [
|
|
|
34
37
|
'MatchResult',
|
|
35
38
|
'MatchStatus',
|
|
36
39
|
'PatternError',
|
|
40
|
+
'Channel',
|
|
37
41
|
'has_fsdb_support',
|
|
38
42
|
]
|
|
39
43
|
|
|
@@ -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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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,
|
|
106
|
+
self._steps.append(WaitStep(cond=cond, require=require, channel=channel, tick=tick))
|
|
62
107
|
return self
|
|
63
108
|
|
|
64
|
-
def delay(
|
|
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
|
-
|
|
73
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -10,6 +10,7 @@ from .result import MatchResult, MatchStatus
|
|
|
10
10
|
from .steps import (
|
|
11
11
|
BranchStep,
|
|
12
12
|
CaptureStep,
|
|
13
|
+
Channel,
|
|
13
14
|
Condition,
|
|
14
15
|
DelayStep,
|
|
15
16
|
LoopStep,
|
|
@@ -58,8 +59,26 @@ def _eval_signal(signal, index: int, captures: dict) -> Any:
|
|
|
58
59
|
raise TypeError(f'Invalid signal type: {type(signal)}')
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
def _eval_channel(channel, index: int, captures: dict) -> Channel | None:
|
|
63
|
+
"""Resolve a channel parameter to a :class:`Channel` instance (or ``None``).
|
|
64
|
+
|
|
65
|
+
Static :class:`Channel` instances are returned as-is. Callables are
|
|
66
|
+
invoked with ``(index, captures)`` and must return a :class:`Channel`.
|
|
67
|
+
"""
|
|
68
|
+
if channel is None:
|
|
69
|
+
return None
|
|
70
|
+
if isinstance(channel, Channel):
|
|
71
|
+
return channel
|
|
72
|
+
if callable(channel):
|
|
73
|
+
result = channel(index, captures)
|
|
74
|
+
if not isinstance(result, Channel):
|
|
75
|
+
raise TypeError(f'channel callable must return a Channel instance, got {type(result)}')
|
|
76
|
+
return result
|
|
77
|
+
raise TypeError(f'Invalid channel type: {type(channel)}')
|
|
78
|
+
|
|
79
|
+
|
|
61
80
|
# ---------------------------------------------------------------------------
|
|
62
|
-
# Waveform
|
|
81
|
+
# Waveform & channel collection
|
|
63
82
|
# ---------------------------------------------------------------------------
|
|
64
83
|
|
|
65
84
|
|
|
@@ -70,11 +89,11 @@ def _collect_waveforms(steps: list[Step]) -> list[Waveform]:
|
|
|
70
89
|
if isinstance(step, WaitStep):
|
|
71
90
|
if isinstance(step.cond, Waveform):
|
|
72
91
|
result.append(step.cond)
|
|
73
|
-
if isinstance(step.
|
|
74
|
-
result.append(step.
|
|
92
|
+
if isinstance(step.require, Waveform):
|
|
93
|
+
result.append(step.require)
|
|
75
94
|
elif isinstance(step, DelayStep):
|
|
76
|
-
if isinstance(step.
|
|
77
|
-
result.append(step.
|
|
95
|
+
if isinstance(step.require, Waveform):
|
|
96
|
+
result.append(step.require)
|
|
78
97
|
elif isinstance(step, CaptureStep):
|
|
79
98
|
if isinstance(step.signal, Waveform):
|
|
80
99
|
result.append(step.signal)
|
|
@@ -122,10 +141,13 @@ def _validate_waveforms(waveforms: list[Waveform]) -> None:
|
|
|
122
141
|
class PatternEngine:
|
|
123
142
|
"""NFA-style engine that scans a time axis and runs pattern instances."""
|
|
124
143
|
|
|
144
|
+
_next_epoch: int = 0
|
|
145
|
+
|
|
125
146
|
def __init__(self, pattern) -> None:
|
|
126
147
|
self._steps: list[Step] = pattern._steps
|
|
127
148
|
self._timeout_cycles: int | None = pattern._timeout_cycles
|
|
128
|
-
|
|
149
|
+
PatternEngine._next_epoch += 1
|
|
150
|
+
self._epoch: int = PatternEngine._next_epoch
|
|
129
151
|
|
|
130
152
|
def run(
|
|
131
153
|
self,
|
|
@@ -138,12 +160,18 @@ class PatternEngine:
|
|
|
138
160
|
ref_wf = waveforms[0]
|
|
139
161
|
n = len(ref_wf.value)
|
|
140
162
|
|
|
163
|
+
# Channel consumption is keyed on (engine epoch, cycle), so stale
|
|
164
|
+
# state from a previous run() is automatically invalidated — no
|
|
165
|
+
# explicit reset is needed for either static or dynamic channels.
|
|
166
|
+
|
|
141
167
|
# ---- determine trigger mode ----
|
|
142
|
-
# If first step is a WaitStep
|
|
143
|
-
#
|
|
168
|
+
# If the first step is a plain WaitStep without an explicit channel,
|
|
169
|
+
# use its cond as the fork trigger and skip it on entry. Wait steps
|
|
170
|
+
# with an explicit channel must go through normal step processing so
|
|
171
|
+
# the channel is consumed correctly.
|
|
144
172
|
skip_first_wait = False
|
|
145
173
|
trigger: Condition | None = None
|
|
146
|
-
if self._steps and isinstance(self._steps[0], WaitStep):
|
|
174
|
+
if self._steps and isinstance(self._steps[0], WaitStep) and self._steps[0].channel is None:
|
|
147
175
|
trigger = self._steps[0].cond
|
|
148
176
|
skip_first_wait = True
|
|
149
177
|
|
|
@@ -170,9 +198,6 @@ class PatternEngine:
|
|
|
170
198
|
completed: list[Instance] = []
|
|
171
199
|
|
|
172
200
|
for t in range(start_idx, end_idx):
|
|
173
|
-
# Reset per-cycle channel consumption tracking (oldest instance wins)
|
|
174
|
-
self._consumed_channels = set()
|
|
175
|
-
|
|
176
201
|
# Tick all active instances in creation order (oldest first)
|
|
177
202
|
still_active: list[Instance] = []
|
|
178
203
|
for inst in active:
|
|
@@ -245,8 +270,9 @@ class PatternEngine:
|
|
|
245
270
|
"""Advance the instance, consuming at most *blocking_budget* blocking steps.
|
|
246
271
|
|
|
247
272
|
Instances are always processed oldest-first within a cycle, so the first
|
|
248
|
-
instance to reach a
|
|
249
|
-
|
|
273
|
+
instance to reach a wait step on a given channel wins; later instances
|
|
274
|
+
see the channel marked consumed for this ``(epoch, cycle)`` and remain
|
|
275
|
+
blocked for the cycle.
|
|
250
276
|
"""
|
|
251
277
|
epsilon_count = 0
|
|
252
278
|
|
|
@@ -280,21 +306,31 @@ class PatternEngine:
|
|
|
280
306
|
# -- blocking steps --
|
|
281
307
|
|
|
282
308
|
if isinstance(step, WaitStep):
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
309
|
+
eff_channel = _eval_channel(step.channel, t, inst.captures) or step._auto_channel
|
|
310
|
+
channel_free = (
|
|
311
|
+
eff_channel._consumed_epoch != self._epoch or eff_channel._consumed_at != t
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# tick=True needs blocking budget and consumes a cycle on
|
|
315
|
+
# match; tick=False is gate-free and stays in the same cycle.
|
|
316
|
+
if (
|
|
317
|
+
(not step.tick or blocking_budget > 0)
|
|
318
|
+
and channel_free
|
|
319
|
+
and _eval_bool(step.cond, t, inst.captures)
|
|
320
|
+
):
|
|
321
|
+
eff_channel._consumed_epoch = self._epoch
|
|
322
|
+
eff_channel._consumed_at = t
|
|
289
323
|
frame.step_idx += 1
|
|
290
|
-
|
|
324
|
+
if step.tick:
|
|
325
|
+
blocking_budget -= 1
|
|
291
326
|
continue
|
|
292
|
-
|
|
293
|
-
|
|
327
|
+
|
|
328
|
+
# Still waiting — check require condition
|
|
329
|
+
if step.require is not None and not _eval_bool(step.require, t, inst.captures):
|
|
294
330
|
inst.status = MatchStatus.REQUIRE_VIOLATED
|
|
295
331
|
inst.end_cycle = int(ref_wf.clock[t])
|
|
296
332
|
return
|
|
297
|
-
return # wait for next tick (budget exhausted or
|
|
333
|
+
return # wait for next tick (budget exhausted, channel busy, or cond false)
|
|
298
334
|
|
|
299
335
|
if isinstance(step, DelayStep):
|
|
300
336
|
if step.remaining is None:
|
|
@@ -306,7 +342,7 @@ class PatternEngine:
|
|
|
306
342
|
continue
|
|
307
343
|
if blocking_budget <= 0:
|
|
308
344
|
return
|
|
309
|
-
if step.
|
|
345
|
+
if step.require is not None and not _eval_bool(step.require, t, inst.captures):
|
|
310
346
|
inst.status = MatchStatus.REQUIRE_VIOLATED
|
|
311
347
|
inst.end_cycle = int(ref_wf.clock[t])
|
|
312
348
|
return
|
|
@@ -321,13 +357,15 @@ class PatternEngine:
|
|
|
321
357
|
# -- epsilon steps --
|
|
322
358
|
|
|
323
359
|
if isinstance(step, CaptureStep):
|
|
324
|
-
name = step.name
|
|
325
360
|
val = _eval_signal(step.signal, t, inst.captures)
|
|
326
|
-
if
|
|
327
|
-
|
|
328
|
-
|
|
361
|
+
if step.mode == 'last':
|
|
362
|
+
inst.captures[step.name] = val
|
|
363
|
+
elif step.mode == 'first':
|
|
364
|
+
inst.captures.setdefault(step.name, val)
|
|
365
|
+
elif step.mode == 'list':
|
|
366
|
+
inst.captures.setdefault(step.name, []).append(val)
|
|
329
367
|
else:
|
|
330
|
-
|
|
368
|
+
raise PatternError(f'Unknown capture mode: {step.mode!r}')
|
|
331
369
|
frame.step_idx += 1
|
|
332
370
|
continue
|
|
333
371
|
|