wavekit 0.6.0__tar.gz → 0.7.0a1__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.6.0 → wavekit-0.7.0a1}/PKG-INFO +140 -33
- {wavekit-0.6.0 → wavekit-0.7.0a1}/README.md +139 -32
- {wavekit-0.6.0 → wavekit-0.7.0a1}/build.py +9 -2
- {wavekit-0.6.0 → wavekit-0.7.0a1}/pyproject.toml +10 -2
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/pattern/__init__.py +1 -1
- wavekit-0.7.0a1/src/wavekit/pattern/compiler.py +172 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/pattern/dsl.py +118 -51
- wavekit-0.7.0a1/src/wavekit/pattern/errors.py +5 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/pattern/result.py +36 -3
- wavekit-0.7.0a1/src/wavekit/pattern/runtime.py +526 -0
- wavekit-0.7.0a1/src/wavekit/pattern/steps.py +179 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/readers/base.py +323 -6
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +52 -21
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/readers/fsdb/reader.py +27 -83
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/readers/fst/reader.py +41 -96
- wavekit-0.7.0a1/src/wavekit/readers/vcd/reader.py +161 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/waveform.py +2 -2
- wavekit-0.6.0/src/wavekit/pattern/engine.py +0 -476
- wavekit-0.6.0/src/wavekit/pattern/instance.py +0 -28
- wavekit-0.6.0/src/wavekit/pattern/steps.py +0 -216
- wavekit-0.6.0/src/wavekit/readers/vcd/reader.py +0 -204
- {wavekit-0.6.0 → wavekit-0.7.0a1}/LICENSE +0 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/__init__.py +0 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/readers/expr_parser.py +0 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/readers/pattern_parser.py +0 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/readers/value_change.pyx +0 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/src/wavekit/scope.py +0 -0
- {wavekit-0.6.0 → wavekit-0.7.0a1}/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.7.0a1
|
|
4
4
|
Summary: a fundamental package for digital circuit waveform analysis
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: cxzzzz
|
|
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
[](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
|
|
26
26
|
[](https://pypi.org/project/wavekit/)
|
|
27
27
|
[](https://pypi.org/project/wavekit/)
|
|
28
|
-
[](https://pepy.tech/project/wavekit)
|
|
29
29
|
[](LICENSE)
|
|
30
30
|
|
|
31
31
|
English | [中文](README_ZH.md)
|
|
@@ -38,7 +38,7 @@ English | [中文](README_ZH.md)
|
|
|
38
38
|
|
|
39
39
|
- **Flexible Signal Extraction**: Flexible batch signal extraction via brace expansion, integer ranges, and regular expressions — load groups of related signals in one call.
|
|
40
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.
|
|
41
|
-
- **Pattern Matching**:
|
|
41
|
+
- **Pattern Matching**: Unified temporal pattern runtime that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
|
|
42
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.
|
|
43
43
|
|
|
44
44
|
## 📦 Installation
|
|
@@ -106,6 +106,25 @@ with VcdReader("fifo_tb.vcd") as f:
|
|
|
106
106
|
burst_cycles = wr_en.rising_edge()
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
To inspect unknown/high-impedance source bits without changing the ordinary
|
|
110
|
+
two-state value model, load an unsigned unknown mask alongside the value
|
|
111
|
+
waveform. Each mask bit is `1` where the source sample contained `X` or `Z`.
|
|
112
|
+
|
|
113
|
+
> **Experimental**: The `load_unknown_mask` / `load_matched_unknown_masks`
|
|
114
|
+
> APIs are experimental and may change in a future release.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from wavekit import VcdReader
|
|
118
|
+
|
|
119
|
+
with VcdReader("fifo_tb.vcd") as f:
|
|
120
|
+
clock = "fifo_tb.clk"
|
|
121
|
+
data = f.load_waveform("fifo_tb.s_fifo.data[7:0]", clock=clock, xz_value=0)
|
|
122
|
+
unknown = f.load_unknown_mask("fifo_tb.s_fifo.data[7:0]", clock=clock)
|
|
123
|
+
|
|
124
|
+
# Keep only samples whose source bits were fully known.
|
|
125
|
+
known_data = data.mask(unknown == 0)
|
|
126
|
+
```
|
|
127
|
+
|
|
109
128
|
---
|
|
110
129
|
|
|
111
130
|
### 3. Expression Evaluation
|
|
@@ -135,9 +154,16 @@ with VcdReader("fifo_tb.vcd") as f:
|
|
|
135
154
|
|
|
136
155
|
### 4. Pattern Matching
|
|
137
156
|
|
|
138
|
-
|
|
157
|
+
`Pattern` scans a waveform and extracts all matching transactions — a request/response pair, a burst, a stall interval, or any other repeating timing pattern.
|
|
158
|
+
|
|
159
|
+
There are two ways to describe a pattern:
|
|
160
|
+
|
|
161
|
+
- **Declarative** — chain steps like `.wait()`, `.consume()`, `.capture()`, `.loop()`. Best for fixed transaction flows.
|
|
162
|
+
- **Programmable** — pass a handler function to `Pattern(...)`. Best for dynamic branches, per-ID routing, and other complex flows.
|
|
139
163
|
|
|
140
|
-
|
|
164
|
+
#### Declarative examples
|
|
165
|
+
|
|
166
|
+
**AXI-Lite read latency**
|
|
141
167
|
|
|
142
168
|
```python
|
|
143
169
|
from wavekit import VcdReader, Pattern
|
|
@@ -150,38 +176,37 @@ with VcdReader("axi_tb.vcd") as f:
|
|
|
150
176
|
rready = f.load_waveform("tb.dut.rready", clock=clk)
|
|
151
177
|
rdata = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)
|
|
152
178
|
|
|
153
|
-
result = (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
179
|
+
result = (
|
|
180
|
+
Pattern(timeout=256)
|
|
181
|
+
.wait(arvalid & arready) # AR handshake → transaction starts
|
|
182
|
+
.wait(rvalid & rready) # R handshake → transaction ends
|
|
183
|
+
.capture("rdata", rdata)
|
|
184
|
+
.match()
|
|
185
|
+
)
|
|
161
186
|
|
|
162
|
-
|
|
163
|
-
print(f"Read latencies (cycles): {
|
|
164
|
-
print(f"Read data: {
|
|
187
|
+
ok = result.filter_ok()
|
|
188
|
+
print(f"Read latencies (cycles): {ok.duration.value}")
|
|
189
|
+
print(f"Read data: {ok.captures['rdata'].value}")
|
|
165
190
|
```
|
|
166
191
|
|
|
167
|
-
**AXI
|
|
192
|
+
**AXI write burst (multi-beat)**
|
|
168
193
|
|
|
169
194
|
```python
|
|
170
|
-
beat = Pattern().
|
|
195
|
+
beat = Pattern().consume(wvalid & wready, channel="w").capture("beats", wdata, mode="list")
|
|
171
196
|
|
|
172
197
|
result = (
|
|
173
198
|
Pattern()
|
|
174
|
-
.wait(awvalid & awready) # AW handshake → burst
|
|
175
|
-
.loop(beat, until=wlast) # collect
|
|
199
|
+
.wait(awvalid & awready) # AW handshake → burst starts
|
|
200
|
+
.loop(beat, until=wlast) # collect each beat until wlast
|
|
176
201
|
.timeout(512)
|
|
177
202
|
.match()
|
|
178
203
|
)
|
|
179
204
|
|
|
180
|
-
for i, inst in enumerate(result.
|
|
205
|
+
for i, inst in enumerate(result.filter_ok()):
|
|
181
206
|
print(f"Burst {i}: {len(inst.captures['beats'])} beats")
|
|
182
207
|
```
|
|
183
208
|
|
|
184
|
-
**Stall
|
|
209
|
+
**Stall detection**
|
|
185
210
|
|
|
186
211
|
```python
|
|
187
212
|
stall = valid & (ready == 0)
|
|
@@ -189,14 +214,51 @@ stall = valid & (ready == 0)
|
|
|
189
214
|
result = (
|
|
190
215
|
Pattern()
|
|
191
216
|
.wait(stall.rising_edge()) # stall begins
|
|
192
|
-
.loop(Pattern().delay(1), when=stall) #
|
|
217
|
+
.loop(Pattern().delay(1), when=stall) # keep waiting until stall ends
|
|
193
218
|
.match()
|
|
194
219
|
)
|
|
195
220
|
|
|
196
|
-
stalls = result.
|
|
221
|
+
stalls = result.filter_ok()
|
|
197
222
|
print(f"Stall durations: {stalls.duration.value} cycles")
|
|
198
223
|
```
|
|
199
224
|
|
|
225
|
+
#### Programmable example
|
|
226
|
+
|
|
227
|
+
**Out-of-order AXI reads by ID**
|
|
228
|
+
|
|
229
|
+
When R beats from different IDs interleave on the bus, match each AR to its response beats by `arid` and collect results as Python dicts.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
arfire = arvalid & arready # precompute outside the handler
|
|
233
|
+
rfire = rvalid & rready
|
|
234
|
+
|
|
235
|
+
async def read_burst(ctx):
|
|
236
|
+
if ctx.value(arfire):
|
|
237
|
+
my_id = ctx.value(arid)
|
|
238
|
+
beats = []
|
|
239
|
+
|
|
240
|
+
while True:
|
|
241
|
+
await ctx.consume(
|
|
242
|
+
lambda: ctx.value(rfire) and ctx.value(rid) == my_id,
|
|
243
|
+
channel=("r", my_id),
|
|
244
|
+
)
|
|
245
|
+
beats.append(int(ctx.value(rdata)))
|
|
246
|
+
if ctx.value(rlast):
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
return {"arid": my_id, "beats": beats}
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
records = Pattern(read_burst, timeout=64).collect()
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Some tips for programmable patterns:
|
|
256
|
+
|
|
257
|
+
- Precompute fixed waveform expressions (like `fire = valid & ready`) outside
|
|
258
|
+
the handler function so they aren't rebuilt every cycle.
|
|
259
|
+
- Start the handler with `if ctx.value(fire): ...` and `return None`
|
|
260
|
+
otherwise — this tells the runtime which cycles begin a transaction.
|
|
261
|
+
|
|
200
262
|
---
|
|
201
263
|
|
|
202
264
|
## 📖 API Reference
|
|
@@ -207,7 +269,9 @@ print(f"Stall durations: {stalls.duration.value} cycles")
|
|
|
207
269
|
|--------|-------------|
|
|
208
270
|
| `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`). |
|
|
209
271
|
| `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
|
|
272
|
+
| `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
|
|
210
273
|
| `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
|
|
274
|
+
| `reader.load_matched_unknown_masks(pattern, clock_pattern, ...)` | **Experimental.** Batch-load X/Z masks for matched signals. Returns `dict[tuple, Waveform]`. |
|
|
211
275
|
| `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
|
|
212
276
|
| `reader.get_matched_signals(pattern)` | Resolve a pattern to signal paths without loading data. |
|
|
213
277
|
| `reader.top_scope_list()` | Return root `Scope` nodes of the signal hierarchy. |
|
|
@@ -290,9 +354,21 @@ changed = wave != wave.back(3)
|
|
|
290
354
|
|
|
291
355
|
### Pattern
|
|
292
356
|
|
|
357
|
+
**Construction**
|
|
358
|
+
|
|
359
|
+
| API | Description |
|
|
360
|
+
|-----|-------------|
|
|
361
|
+
| `Pattern(timeout=..., max_active=...)` | Create a Declarative Pattern. Add steps with builder methods, then call `.match()`. |
|
|
362
|
+
| `Pattern(async_fn, timeout=..., max_active=...)` | Create a Programmable Pattern. The async function receives `ctx`. |
|
|
363
|
+
| `.match(start_cycle=None, end_cycle=None)` | Run the pattern and return `MatchResult`. In a Programmable Pattern, return `ctx.OK` for success and `None` to skip. |
|
|
364
|
+
| `.collect(start_cycle=None, end_cycle=None)` | Programmable Pattern only. Collect each non-`None` Python return value. |
|
|
365
|
+
|
|
366
|
+
**Declarative Steps**
|
|
367
|
+
|
|
293
368
|
| Method | Description |
|
|
294
369
|
|--------|-------------|
|
|
295
|
-
| `.wait(cond, *, require=None
|
|
370
|
+
| `.wait(cond, *, require=None)` | Block until `cond` is True without consuming the event. Resumes in the same cycle when already true; use `.delay(1)` for next-cycle behavior. `require` is checked each waiting cycle (failure → `REQUIRE_VIOLATED`). |
|
|
371
|
+
| `.consume(cond, channel, *, require=None)` | Block until `cond` is True and this instance can exclusively consume from `channel`. Resumes in the same cycle on success. Use this for FIFO request/response pairing and per-key routing. |
|
|
296
372
|
| `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
|
|
297
373
|
| `.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. |
|
|
298
374
|
| `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
|
|
@@ -300,27 +376,54 @@ changed = wave != wave.back(3)
|
|
|
300
376
|
| `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
|
|
301
377
|
| `.branch(cond, true_body, false_body)` | Conditional branch. |
|
|
302
378
|
| `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
|
|
303
|
-
| `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
|
|
304
379
|
|
|
305
|
-
|
|
380
|
+
The same time and ownership operations are available inside Programmable
|
|
381
|
+
Patterns as `await ctx.wait(...)`, `await ctx.consume(...)`, and
|
|
382
|
+
`await ctx.delay(...)`.
|
|
383
|
+
|
|
384
|
+
**Programmable Context**
|
|
385
|
+
|
|
386
|
+
| API | Description |
|
|
387
|
+
|-----|-------------|
|
|
388
|
+
| `ctx.value(waveform, offset=0)` | Read a scalar value at the current sample plus optional offset. |
|
|
389
|
+
| `ctx.cycle(waveform, offset=0)` | Read the cycle number at the current sample plus optional offset. |
|
|
390
|
+
| `ctx.time(waveform, offset=0)` | Read the timestamp at the current sample plus optional offset. |
|
|
391
|
+
| `await ctx.wait(cond, require=None)` | Observe cycles until `cond` is true; does not consume the event. |
|
|
392
|
+
| `await ctx.consume(cond, channel, require=None)` | Wait for `cond` and exclusively consume from `channel`. |
|
|
393
|
+
| `await ctx.delay(n, require=None)` | Advance `n` cycles. |
|
|
394
|
+
| `ctx.capture(name, value, mode='last')` | Record a capture for programmable `.match()`. |
|
|
395
|
+
| `ctx.OK` | Return from programmable `.match()` to record a successful match. |
|
|
396
|
+
|
|
397
|
+
**Dynamic callbacks**
|
|
398
|
+
|
|
399
|
+
Declarative callbacks use `callable(index, captures)`. `index` is the absolute
|
|
400
|
+
waveform sample index; it is not rebased when `match(start_cycle=...)` is used.
|
|
401
|
+
|
|
402
|
+
**Channels and consume vs. wait**
|
|
403
|
+
|
|
404
|
+
When multiple in-flight instances are waiting for the same kind of event, plain
|
|
405
|
+
`wait()` won't pair each request with its own response — every instance sees
|
|
406
|
+
every event. `consume()` solves this: it hands the event to exactly one instance
|
|
407
|
+
per cycle, in FIFO order.
|
|
306
408
|
|
|
307
|
-
A `Channel` is
|
|
409
|
+
A `Channel` is the FIFO queue that `consume()` uses. Pass a `Channel` object, a
|
|
410
|
+
hashable key, or a dynamic callback to `consume(..., channel=...)`. All
|
|
411
|
+
instances sharing the same channel key consume from the same queue.
|
|
308
412
|
|
|
309
413
|
```python
|
|
310
414
|
from collections import defaultdict
|
|
311
415
|
from wavekit import Channel, Pattern
|
|
312
416
|
|
|
313
417
|
# Multi-bank cache: each bank has its own response port, so two banks
|
|
314
|
-
# can return data in the *same* cycle.
|
|
315
|
-
#
|
|
316
|
-
# A per-bank Channel lets each in-flight read consume from its own bank.
|
|
418
|
+
# can return data in the *same* cycle. A per-bank Channel lets each in-flight
|
|
419
|
+
# read consume from its own bank while preserving FIFO order within that bank.
|
|
317
420
|
banks = defaultdict(Channel)
|
|
318
421
|
|
|
319
422
|
result = (
|
|
320
423
|
Pattern()
|
|
321
424
|
.wait(req_valid)
|
|
322
425
|
.capture('bank', req_addr & 1)
|
|
323
|
-
.
|
|
426
|
+
.consume(
|
|
324
427
|
lambda i, cap: bank_valid[cap['bank']].value[i],
|
|
325
428
|
channel=lambda i, cap: banks[cap['bank']],
|
|
326
429
|
)
|
|
@@ -338,7 +441,11 @@ result = (
|
|
|
338
441
|
| `.duration` | `end - start + 1` cycles. |
|
|
339
442
|
| `.status` | `MatchStatus.OK`, `TIMEOUT`, or `REQUIRE_VIOLATED`. |
|
|
340
443
|
| `.captures` | `dict[str, Waveform]` of captured values. |
|
|
341
|
-
| `.
|
|
444
|
+
| `.ok` | Boolean Waveform where `status == MatchStatus.OK`. |
|
|
445
|
+
| `.failed` | Boolean Waveform where `status != MatchStatus.OK`. |
|
|
446
|
+
| `.filter_ok()` | Return only `OK` matches. |
|
|
447
|
+
| `.filter_status(status)` | Return only matches with the given `MatchStatus` or integer status. |
|
|
448
|
+
| `.filter_failed()` | Return only non-OK matches. |
|
|
342
449
|
|
|
343
450
|
---
|
|
344
451
|
|
|
@@ -3,7 +3,7 @@
|
|
|
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://pepy.tech/project/wavekit)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
9
|
English | [中文](README_ZH.md)
|
|
@@ -16,7 +16,7 @@ English | [中文](README_ZH.md)
|
|
|
16
16
|
|
|
17
17
|
- **Flexible Signal Extraction**: Flexible batch signal extraction via brace expansion, integer ranges, and regular expressions — load groups of related signals in one call.
|
|
18
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.
|
|
19
|
-
- **Pattern Matching**:
|
|
19
|
+
- **Pattern Matching**: Unified temporal pattern runtime that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
|
|
20
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.
|
|
21
21
|
|
|
22
22
|
## 📦 Installation
|
|
@@ -84,6 +84,25 @@ with VcdReader("fifo_tb.vcd") as f:
|
|
|
84
84
|
burst_cycles = wr_en.rising_edge()
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
To inspect unknown/high-impedance source bits without changing the ordinary
|
|
88
|
+
two-state value model, load an unsigned unknown mask alongside the value
|
|
89
|
+
waveform. Each mask bit is `1` where the source sample contained `X` or `Z`.
|
|
90
|
+
|
|
91
|
+
> **Experimental**: The `load_unknown_mask` / `load_matched_unknown_masks`
|
|
92
|
+
> APIs are experimental and may change in a future release.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from wavekit import VcdReader
|
|
96
|
+
|
|
97
|
+
with VcdReader("fifo_tb.vcd") as f:
|
|
98
|
+
clock = "fifo_tb.clk"
|
|
99
|
+
data = f.load_waveform("fifo_tb.s_fifo.data[7:0]", clock=clock, xz_value=0)
|
|
100
|
+
unknown = f.load_unknown_mask("fifo_tb.s_fifo.data[7:0]", clock=clock)
|
|
101
|
+
|
|
102
|
+
# Keep only samples whose source bits were fully known.
|
|
103
|
+
known_data = data.mask(unknown == 0)
|
|
104
|
+
```
|
|
105
|
+
|
|
87
106
|
---
|
|
88
107
|
|
|
89
108
|
### 3. Expression Evaluation
|
|
@@ -113,9 +132,16 @@ with VcdReader("fifo_tb.vcd") as f:
|
|
|
113
132
|
|
|
114
133
|
### 4. Pattern Matching
|
|
115
134
|
|
|
116
|
-
|
|
135
|
+
`Pattern` scans a waveform and extracts all matching transactions — a request/response pair, a burst, a stall interval, or any other repeating timing pattern.
|
|
136
|
+
|
|
137
|
+
There are two ways to describe a pattern:
|
|
138
|
+
|
|
139
|
+
- **Declarative** — chain steps like `.wait()`, `.consume()`, `.capture()`, `.loop()`. Best for fixed transaction flows.
|
|
140
|
+
- **Programmable** — pass a handler function to `Pattern(...)`. Best for dynamic branches, per-ID routing, and other complex flows.
|
|
117
141
|
|
|
118
|
-
|
|
142
|
+
#### Declarative examples
|
|
143
|
+
|
|
144
|
+
**AXI-Lite read latency**
|
|
119
145
|
|
|
120
146
|
```python
|
|
121
147
|
from wavekit import VcdReader, Pattern
|
|
@@ -128,38 +154,37 @@ with VcdReader("axi_tb.vcd") as f:
|
|
|
128
154
|
rready = f.load_waveform("tb.dut.rready", clock=clk)
|
|
129
155
|
rdata = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)
|
|
130
156
|
|
|
131
|
-
result = (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
157
|
+
result = (
|
|
158
|
+
Pattern(timeout=256)
|
|
159
|
+
.wait(arvalid & arready) # AR handshake → transaction starts
|
|
160
|
+
.wait(rvalid & rready) # R handshake → transaction ends
|
|
161
|
+
.capture("rdata", rdata)
|
|
162
|
+
.match()
|
|
163
|
+
)
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
print(f"Read latencies (cycles): {
|
|
142
|
-
print(f"Read data: {
|
|
165
|
+
ok = result.filter_ok()
|
|
166
|
+
print(f"Read latencies (cycles): {ok.duration.value}")
|
|
167
|
+
print(f"Read data: {ok.captures['rdata'].value}")
|
|
143
168
|
```
|
|
144
169
|
|
|
145
|
-
**AXI
|
|
170
|
+
**AXI write burst (multi-beat)**
|
|
146
171
|
|
|
147
172
|
```python
|
|
148
|
-
beat = Pattern().
|
|
173
|
+
beat = Pattern().consume(wvalid & wready, channel="w").capture("beats", wdata, mode="list")
|
|
149
174
|
|
|
150
175
|
result = (
|
|
151
176
|
Pattern()
|
|
152
|
-
.wait(awvalid & awready) # AW handshake → burst
|
|
153
|
-
.loop(beat, until=wlast) # collect
|
|
177
|
+
.wait(awvalid & awready) # AW handshake → burst starts
|
|
178
|
+
.loop(beat, until=wlast) # collect each beat until wlast
|
|
154
179
|
.timeout(512)
|
|
155
180
|
.match()
|
|
156
181
|
)
|
|
157
182
|
|
|
158
|
-
for i, inst in enumerate(result.
|
|
183
|
+
for i, inst in enumerate(result.filter_ok()):
|
|
159
184
|
print(f"Burst {i}: {len(inst.captures['beats'])} beats")
|
|
160
185
|
```
|
|
161
186
|
|
|
162
|
-
**Stall
|
|
187
|
+
**Stall detection**
|
|
163
188
|
|
|
164
189
|
```python
|
|
165
190
|
stall = valid & (ready == 0)
|
|
@@ -167,14 +192,51 @@ stall = valid & (ready == 0)
|
|
|
167
192
|
result = (
|
|
168
193
|
Pattern()
|
|
169
194
|
.wait(stall.rising_edge()) # stall begins
|
|
170
|
-
.loop(Pattern().delay(1), when=stall) #
|
|
195
|
+
.loop(Pattern().delay(1), when=stall) # keep waiting until stall ends
|
|
171
196
|
.match()
|
|
172
197
|
)
|
|
173
198
|
|
|
174
|
-
stalls = result.
|
|
199
|
+
stalls = result.filter_ok()
|
|
175
200
|
print(f"Stall durations: {stalls.duration.value} cycles")
|
|
176
201
|
```
|
|
177
202
|
|
|
203
|
+
#### Programmable example
|
|
204
|
+
|
|
205
|
+
**Out-of-order AXI reads by ID**
|
|
206
|
+
|
|
207
|
+
When R beats from different IDs interleave on the bus, match each AR to its response beats by `arid` and collect results as Python dicts.
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
arfire = arvalid & arready # precompute outside the handler
|
|
211
|
+
rfire = rvalid & rready
|
|
212
|
+
|
|
213
|
+
async def read_burst(ctx):
|
|
214
|
+
if ctx.value(arfire):
|
|
215
|
+
my_id = ctx.value(arid)
|
|
216
|
+
beats = []
|
|
217
|
+
|
|
218
|
+
while True:
|
|
219
|
+
await ctx.consume(
|
|
220
|
+
lambda: ctx.value(rfire) and ctx.value(rid) == my_id,
|
|
221
|
+
channel=("r", my_id),
|
|
222
|
+
)
|
|
223
|
+
beats.append(int(ctx.value(rdata)))
|
|
224
|
+
if ctx.value(rlast):
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
return {"arid": my_id, "beats": beats}
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
records = Pattern(read_burst, timeout=64).collect()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Some tips for programmable patterns:
|
|
234
|
+
|
|
235
|
+
- Precompute fixed waveform expressions (like `fire = valid & ready`) outside
|
|
236
|
+
the handler function so they aren't rebuilt every cycle.
|
|
237
|
+
- Start the handler with `if ctx.value(fire): ...` and `return None`
|
|
238
|
+
otherwise — this tells the runtime which cycles begin a transaction.
|
|
239
|
+
|
|
178
240
|
---
|
|
179
241
|
|
|
180
242
|
## 📖 API Reference
|
|
@@ -185,7 +247,9 @@ print(f"Stall durations: {stalls.duration.value} cycles")
|
|
|
185
247
|
|--------|-------------|
|
|
186
248
|
| `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`). |
|
|
187
249
|
| `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
|
|
250
|
+
| `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
|
|
188
251
|
| `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
|
|
252
|
+
| `reader.load_matched_unknown_masks(pattern, clock_pattern, ...)` | **Experimental.** Batch-load X/Z masks for matched signals. Returns `dict[tuple, Waveform]`. |
|
|
189
253
|
| `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
|
|
190
254
|
| `reader.get_matched_signals(pattern)` | Resolve a pattern to signal paths without loading data. |
|
|
191
255
|
| `reader.top_scope_list()` | Return root `Scope` nodes of the signal hierarchy. |
|
|
@@ -268,9 +332,21 @@ changed = wave != wave.back(3)
|
|
|
268
332
|
|
|
269
333
|
### Pattern
|
|
270
334
|
|
|
335
|
+
**Construction**
|
|
336
|
+
|
|
337
|
+
| API | Description |
|
|
338
|
+
|-----|-------------|
|
|
339
|
+
| `Pattern(timeout=..., max_active=...)` | Create a Declarative Pattern. Add steps with builder methods, then call `.match()`. |
|
|
340
|
+
| `Pattern(async_fn, timeout=..., max_active=...)` | Create a Programmable Pattern. The async function receives `ctx`. |
|
|
341
|
+
| `.match(start_cycle=None, end_cycle=None)` | Run the pattern and return `MatchResult`. In a Programmable Pattern, return `ctx.OK` for success and `None` to skip. |
|
|
342
|
+
| `.collect(start_cycle=None, end_cycle=None)` | Programmable Pattern only. Collect each non-`None` Python return value. |
|
|
343
|
+
|
|
344
|
+
**Declarative Steps**
|
|
345
|
+
|
|
271
346
|
| Method | Description |
|
|
272
347
|
|--------|-------------|
|
|
273
|
-
| `.wait(cond, *, require=None
|
|
348
|
+
| `.wait(cond, *, require=None)` | Block until `cond` is True without consuming the event. Resumes in the same cycle when already true; use `.delay(1)` for next-cycle behavior. `require` is checked each waiting cycle (failure → `REQUIRE_VIOLATED`). |
|
|
349
|
+
| `.consume(cond, channel, *, require=None)` | Block until `cond` is True and this instance can exclusively consume from `channel`. Resumes in the same cycle on success. Use this for FIFO request/response pairing and per-key routing. |
|
|
274
350
|
| `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
|
|
275
351
|
| `.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. |
|
|
276
352
|
| `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
|
|
@@ -278,27 +354,54 @@ changed = wave != wave.back(3)
|
|
|
278
354
|
| `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
|
|
279
355
|
| `.branch(cond, true_body, false_body)` | Conditional branch. |
|
|
280
356
|
| `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
|
|
281
|
-
| `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
|
|
282
357
|
|
|
283
|
-
|
|
358
|
+
The same time and ownership operations are available inside Programmable
|
|
359
|
+
Patterns as `await ctx.wait(...)`, `await ctx.consume(...)`, and
|
|
360
|
+
`await ctx.delay(...)`.
|
|
361
|
+
|
|
362
|
+
**Programmable Context**
|
|
363
|
+
|
|
364
|
+
| API | Description |
|
|
365
|
+
|-----|-------------|
|
|
366
|
+
| `ctx.value(waveform, offset=0)` | Read a scalar value at the current sample plus optional offset. |
|
|
367
|
+
| `ctx.cycle(waveform, offset=0)` | Read the cycle number at the current sample plus optional offset. |
|
|
368
|
+
| `ctx.time(waveform, offset=0)` | Read the timestamp at the current sample plus optional offset. |
|
|
369
|
+
| `await ctx.wait(cond, require=None)` | Observe cycles until `cond` is true; does not consume the event. |
|
|
370
|
+
| `await ctx.consume(cond, channel, require=None)` | Wait for `cond` and exclusively consume from `channel`. |
|
|
371
|
+
| `await ctx.delay(n, require=None)` | Advance `n` cycles. |
|
|
372
|
+
| `ctx.capture(name, value, mode='last')` | Record a capture for programmable `.match()`. |
|
|
373
|
+
| `ctx.OK` | Return from programmable `.match()` to record a successful match. |
|
|
374
|
+
|
|
375
|
+
**Dynamic callbacks**
|
|
376
|
+
|
|
377
|
+
Declarative callbacks use `callable(index, captures)`. `index` is the absolute
|
|
378
|
+
waveform sample index; it is not rebased when `match(start_cycle=...)` is used.
|
|
379
|
+
|
|
380
|
+
**Channels and consume vs. wait**
|
|
381
|
+
|
|
382
|
+
When multiple in-flight instances are waiting for the same kind of event, plain
|
|
383
|
+
`wait()` won't pair each request with its own response — every instance sees
|
|
384
|
+
every event. `consume()` solves this: it hands the event to exactly one instance
|
|
385
|
+
per cycle, in FIFO order.
|
|
284
386
|
|
|
285
|
-
A `Channel` is
|
|
387
|
+
A `Channel` is the FIFO queue that `consume()` uses. Pass a `Channel` object, a
|
|
388
|
+
hashable key, or a dynamic callback to `consume(..., channel=...)`. All
|
|
389
|
+
instances sharing the same channel key consume from the same queue.
|
|
286
390
|
|
|
287
391
|
```python
|
|
288
392
|
from collections import defaultdict
|
|
289
393
|
from wavekit import Channel, Pattern
|
|
290
394
|
|
|
291
395
|
# Multi-bank cache: each bank has its own response port, so two banks
|
|
292
|
-
# can return data in the *same* cycle.
|
|
293
|
-
#
|
|
294
|
-
# A per-bank Channel lets each in-flight read consume from its own bank.
|
|
396
|
+
# can return data in the *same* cycle. A per-bank Channel lets each in-flight
|
|
397
|
+
# read consume from its own bank while preserving FIFO order within that bank.
|
|
295
398
|
banks = defaultdict(Channel)
|
|
296
399
|
|
|
297
400
|
result = (
|
|
298
401
|
Pattern()
|
|
299
402
|
.wait(req_valid)
|
|
300
403
|
.capture('bank', req_addr & 1)
|
|
301
|
-
.
|
|
404
|
+
.consume(
|
|
302
405
|
lambda i, cap: bank_valid[cap['bank']].value[i],
|
|
303
406
|
channel=lambda i, cap: banks[cap['bank']],
|
|
304
407
|
)
|
|
@@ -316,7 +419,11 @@ result = (
|
|
|
316
419
|
| `.duration` | `end - start + 1` cycles. |
|
|
317
420
|
| `.status` | `MatchStatus.OK`, `TIMEOUT`, or `REQUIRE_VIOLATED`. |
|
|
318
421
|
| `.captures` | `dict[str, Waveform]` of captured values. |
|
|
319
|
-
| `.
|
|
422
|
+
| `.ok` | Boolean Waveform where `status == MatchStatus.OK`. |
|
|
423
|
+
| `.failed` | Boolean Waveform where `status != MatchStatus.OK`. |
|
|
424
|
+
| `.filter_ok()` | Return only `OK` matches. |
|
|
425
|
+
| `.filter_status(status)` | Return only matches with the given `MatchStatus` or integer status. |
|
|
426
|
+
| `.filter_failed()` | Return only non-OK matches. |
|
|
320
427
|
|
|
321
428
|
---
|
|
322
429
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
from Cython.Build import cythonize
|
|
3
5
|
from setuptools import Extension, setup
|
|
4
6
|
|
|
7
|
+
for artifact in Path('src/wavekit').glob('**/*.so'):
|
|
8
|
+
artifact.unlink()
|
|
9
|
+
|
|
5
10
|
extensions = [
|
|
6
11
|
Extension(
|
|
7
|
-
'
|
|
12
|
+
'wavekit.readers.value_change',
|
|
8
13
|
sources=['src/wavekit/readers/value_change.pyx'],
|
|
9
14
|
include_dirs=[np.get_include()],
|
|
10
15
|
extra_compile_args=['-fpic', '-O3', '-march=native'],
|
|
@@ -12,7 +17,7 @@ extensions = [
|
|
|
12
17
|
language='c++',
|
|
13
18
|
),
|
|
14
19
|
Extension(
|
|
15
|
-
'
|
|
20
|
+
'wavekit.readers.fsdb.npi_fsdb_reader',
|
|
16
21
|
sources=['src/wavekit/readers/fsdb/npi_fsdb_reader.pyx'],
|
|
17
22
|
include_dirs=[np.get_include()],
|
|
18
23
|
libraries=['dl'],
|
|
@@ -23,8 +28,10 @@ extensions = [
|
|
|
23
28
|
]
|
|
24
29
|
|
|
25
30
|
setup(
|
|
31
|
+
package_dir={'': 'src'},
|
|
26
32
|
ext_modules=cythonize(
|
|
27
33
|
extensions,
|
|
34
|
+
include_path=['src'],
|
|
28
35
|
compiler_directives={
|
|
29
36
|
'language_level': 3,
|
|
30
37
|
'embedsignature': True,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "wavekit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0a1"
|
|
4
4
|
description = "a fundamental package for digital circuit waveform analysis"
|
|
5
5
|
authors = ["cxzzzz <cxz19961010@outlook.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
repository = "https://github.com/cxzzzz/wavekit"
|
|
8
8
|
build = "build.py"
|
|
9
|
-
include = [
|
|
9
|
+
include = [
|
|
10
|
+
{ path = "src/wavekit/**/*.so", format = "wheel" },
|
|
11
|
+
]
|
|
10
12
|
|
|
11
13
|
[tool.poetry.dependencies]
|
|
12
14
|
python = "^3.9"
|
|
@@ -23,6 +25,7 @@ mypy = "^1.11.2"
|
|
|
23
25
|
target-version = "py39"
|
|
24
26
|
line-length = 100
|
|
25
27
|
src = ["src", "tests"]
|
|
28
|
+
exclude = ["tmp"]
|
|
26
29
|
|
|
27
30
|
[tool.ruff.lint]
|
|
28
31
|
select = ["E", "F", "W", "I", "UP", "B"]
|
|
@@ -34,9 +37,14 @@ select = ["E", "F", "W", "I", "UP", "B"]
|
|
|
34
37
|
quote-style = "single"
|
|
35
38
|
line-ending = "lf"
|
|
36
39
|
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
pythonpath = ["tests"]
|
|
43
|
+
|
|
37
44
|
[tool.mypy]
|
|
38
45
|
python_version = "3.9"
|
|
39
46
|
files = ["src", "tests"]
|
|
47
|
+
exclude = ["^tmp/"]
|
|
40
48
|
warn_unused_ignores = true
|
|
41
49
|
warn_redundant_casts = true
|
|
42
50
|
warn_unused_configs = true
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Protocol pattern matching for waveform analysis."""
|
|
2
2
|
|
|
3
3
|
from .dsl import Pattern as Pattern
|
|
4
|
-
from .
|
|
4
|
+
from .errors import PatternError as PatternError
|
|
5
5
|
from .result import MatchResult as MatchResult
|
|
6
6
|
from .result import MatchStatus as MatchStatus
|
|
7
7
|
from .steps import Channel as Channel
|