wavekit 0.3.0__tar.gz → 0.5.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.0/PKG-INFO +350 -0
- wavekit-0.5.0/README.md +328 -0
- {wavekit-0.3.0 → wavekit-0.5.0}/pyproject.toml +1 -1
- {wavekit-0.3.0 → wavekit-0.5.0}/src/wavekit/__init__.py +15 -1
- wavekit-0.5.0/src/wavekit/pattern/__init__.py +8 -0
- wavekit-0.5.0/src/wavekit/pattern/dsl.py +183 -0
- wavekit-0.5.0/src/wavekit/pattern/engine.py +438 -0
- wavekit-0.5.0/src/wavekit/pattern/instance.py +28 -0
- wavekit-0.5.0/src/wavekit/pattern/result.py +79 -0
- wavekit-0.5.0/src/wavekit/pattern/steps.py +142 -0
- wavekit-0.5.0/src/wavekit/readers/base.py +537 -0
- {wavekit-0.3.0 → wavekit-0.5.0}/src/wavekit/readers/fsdb/reader.py +77 -39
- {wavekit-0.3.0 → wavekit-0.5.0}/src/wavekit/readers/value_change.pyx +12 -9
- wavekit-0.5.0/src/wavekit/readers/vcd/reader.py +189 -0
- wavekit-0.5.0/src/wavekit/scope.py +277 -0
- wavekit-0.5.0/src/wavekit/signal.py +49 -0
- {wavekit-0.3.0 → wavekit-0.5.0}/src/wavekit/waveform.py +468 -23
- wavekit-0.3.0/PKG-INFO +0 -230
- wavekit-0.3.0/README.md +0 -208
- wavekit-0.3.0/src/wavekit/readers/base.py +0 -281
- wavekit-0.3.0/src/wavekit/readers/vcd/reader.py +0 -129
- wavekit-0.3.0/src/wavekit/scope.py +0 -164
- wavekit-0.3.0/src/wavekit/signal.py +0 -35
- {wavekit-0.3.0 → wavekit-0.5.0}/LICENSE +0 -0
- {wavekit-0.3.0 → wavekit-0.5.0}/build.py +0 -0
- {wavekit-0.3.0 → wavekit-0.5.0}/src/wavekit/readers/expr_parser.py +0 -0
- {wavekit-0.3.0 → wavekit-0.5.0}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
- {wavekit-0.3.0 → wavekit-0.5.0}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +0 -0
- {wavekit-0.3.0 → wavekit-0.5.0}/src/wavekit/readers/pattern_parser.py +0 -0
wavekit-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wavekit
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: a fundamental package for digital circuit waveform analysis
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: cxzzzz
|
|
7
|
+
Author-email: cxz19961010@outlook.com
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: numpy (>=2.0.0,<3.0.0)
|
|
17
|
+
Requires-Dist: pytest (>=8.2.2,<9.0.0)
|
|
18
|
+
Requires-Dist: vcdvcd (>=2.3.5,<3.0.0)
|
|
19
|
+
Project-URL: Repository, https://github.com/cxzzzz/wavekit
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# wavekit
|
|
23
|
+
|
|
24
|
+
[](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
|
|
25
|
+
[](https://pypi.org/project/wavekit/)
|
|
26
|
+
[](https://pypi.org/project/wavekit/)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
|
|
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.
|
|
30
|
+
|
|
31
|
+
## ✨ Features
|
|
32
|
+
|
|
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.
|
|
34
|
+
- **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
|
+
- **Pattern Matching**: NFA-based temporal pattern engine that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install wavekit
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Note**: To read FSDB files, ensure the `VERDI_HOME` environment variable is set before installation.
|
|
44
|
+
|
|
45
|
+
## 🚀 Quick Start
|
|
46
|
+
|
|
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.
|
|
48
|
+
|
|
49
|
+
### 1. Batch Signal Extraction
|
|
50
|
+
|
|
51
|
+
Use brace expansion or regular expressions to load multiple related signals in one call.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from wavekit import VcdReader
|
|
55
|
+
|
|
56
|
+
with VcdReader("jtag.vcd") as f:
|
|
57
|
+
# Brace expansion: load J_state and J_next in one call
|
|
58
|
+
# Returns: { ('state',): Waveform, ('next',): Waveform }
|
|
59
|
+
waves = f.load_matched_waveforms(
|
|
60
|
+
"tb.u0.J_{state,next}[3:0]",
|
|
61
|
+
clock_pattern="tb.tck",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Regex mode (@ prefix): capture groups become dict keys
|
|
65
|
+
waves = f.load_matched_waveforms(
|
|
66
|
+
r"tb.u0.@J_([a-z]+)",
|
|
67
|
+
clock_pattern="tb.tck",
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### 2. Signal Analysis
|
|
74
|
+
|
|
75
|
+
Waveforms support Numpy-style arithmetic, masking, and edge detection out of the box.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import numpy as np
|
|
79
|
+
from wavekit import VcdReader
|
|
80
|
+
|
|
81
|
+
with VcdReader("fifo_tb.vcd") as f:
|
|
82
|
+
clock = "fifo_tb.clk"
|
|
83
|
+
depth = 8
|
|
84
|
+
|
|
85
|
+
w_ptr = f.load_waveform("fifo_tb.s_fifo.w_ptr[2:0]", clock=clock)
|
|
86
|
+
r_ptr = f.load_waveform("fifo_tb.s_fifo.r_ptr[2:0]", clock=clock)
|
|
87
|
+
wr_en = f.load_waveform("fifo_tb.s_fifo.wr_en", clock=clock)
|
|
88
|
+
|
|
89
|
+
occupancy = (w_ptr + depth - r_ptr) % depth
|
|
90
|
+
print(f"Average occupancy: {np.mean(occupancy.value):.2f}")
|
|
91
|
+
|
|
92
|
+
# Filter to cycles where a write is active
|
|
93
|
+
write_occ = occupancy.mask(wr_en == 1)
|
|
94
|
+
|
|
95
|
+
# Detect write bursts
|
|
96
|
+
burst_cycles = wr_en.rising_edge()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### 3. Expression Evaluation
|
|
102
|
+
|
|
103
|
+
Compute waveform expressions directly from signal path strings without loading each signal manually.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from wavekit import VcdReader
|
|
107
|
+
|
|
108
|
+
with VcdReader("fifo_tb.vcd") as f:
|
|
109
|
+
# Single mode: paths must each match exactly one signal
|
|
110
|
+
occupancy = f.eval(
|
|
111
|
+
"fifo_tb.s_fifo.w_ptr[2:0] - fifo_tb.s_fifo.r_ptr[2:0]",
|
|
112
|
+
clock="fifo_tb.clk",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Zip mode: brace patterns expand per key, evaluated once per match
|
|
116
|
+
# Returns: { (0,): Waveform, (1,): Waveform, (2,): Waveform, (3,): Waveform }
|
|
117
|
+
occupancies = f.eval(
|
|
118
|
+
"tb.fifo_{0..3}.w_ptr[2:0] - tb.fifo_{0..3}.r_ptr[2:0]",
|
|
119
|
+
clock="tb.clk",
|
|
120
|
+
mode="zip",
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### 4. Pattern Matching
|
|
127
|
+
|
|
128
|
+
Describe a temporal sequence of events; the engine finds all matching transactions in one pass.
|
|
129
|
+
|
|
130
|
+
**AXI-lite Read Latency**
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from wavekit import VcdReader, Pattern
|
|
134
|
+
|
|
135
|
+
with VcdReader("axi_tb.vcd") as f:
|
|
136
|
+
clk = "tb.clk"
|
|
137
|
+
arvalid = f.load_waveform("tb.dut.arvalid", clock=clk)
|
|
138
|
+
arready = f.load_waveform("tb.dut.arready", clock=clk)
|
|
139
|
+
rvalid = f.load_waveform("tb.dut.rvalid", clock=clk)
|
|
140
|
+
rready = f.load_waveform("tb.dut.rready", clock=clk)
|
|
141
|
+
rdata = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)
|
|
142
|
+
|
|
143
|
+
result = (
|
|
144
|
+
Pattern()
|
|
145
|
+
.wait(arvalid & arready) # AR handshake → start
|
|
146
|
+
.wait(rvalid & rready) # R handshake → end
|
|
147
|
+
.capture("rdata", rdata)
|
|
148
|
+
.timeout(256)
|
|
149
|
+
.match()
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
valid = result.filter_valid()
|
|
153
|
+
print(f"Read latencies (cycles): {valid.duration.value}")
|
|
154
|
+
print(f"Read data: {valid.captures['rdata'].value}")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**AXI Write Burst (multi-beat)**
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
beat = Pattern().wait(wvalid & wready).capture("beats[]", wdata)
|
|
161
|
+
|
|
162
|
+
result = (
|
|
163
|
+
Pattern()
|
|
164
|
+
.wait(awvalid & awready) # AW handshake → burst start
|
|
165
|
+
.loop(beat, until=wlast) # collect beats until wlast
|
|
166
|
+
.timeout(512)
|
|
167
|
+
.match()
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
for i, inst in enumerate(result.filter_valid()):
|
|
171
|
+
print(f"Burst {i}: {len(inst.captures['beats'])} beats")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Stall Detection**
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
stall = valid & (ready == 0)
|
|
178
|
+
|
|
179
|
+
result = (
|
|
180
|
+
Pattern()
|
|
181
|
+
.wait(stall.rising_edge()) # stall begins
|
|
182
|
+
.loop(Pattern().delay(1), when=stall) # wait while stalling
|
|
183
|
+
.match()
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
stalls = result.filter_valid()
|
|
187
|
+
print(f"Stall durations: {stalls.duration.value} cycles")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 📖 API Reference
|
|
193
|
+
|
|
194
|
+
### Reader
|
|
195
|
+
|
|
196
|
+
| Method | Description |
|
|
197
|
+
|--------|-------------|
|
|
198
|
+
| `VcdReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires `VERDI_HOME`. |
|
|
199
|
+
| `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
|
|
200
|
+
| `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
|
|
201
|
+
| `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
|
|
202
|
+
| `reader.get_matched_signals(pattern)` | Resolve a pattern to signal paths without loading data. |
|
|
203
|
+
| `reader.top_scope_list()` | Return root `Scope` nodes of the signal hierarchy. |
|
|
204
|
+
|
|
205
|
+
**Pattern syntax** used in signal paths:
|
|
206
|
+
|
|
207
|
+
| Syntax | Example | Effect |
|
|
208
|
+
|--------|---------|--------|
|
|
209
|
+
| `{a,b,c}` | `sig_{read,write}` | Enumerate named variants |
|
|
210
|
+
| `{N..M}` | `fifo_{0..3}.ptr` | Integer range |
|
|
211
|
+
| `{N..M..step}` | `lane_{0..6..2}` | Stepped range |
|
|
212
|
+
| `@<regex>` | `@([a-z]+)_valid` | Regex with capture groups |
|
|
213
|
+
| `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) |
|
|
214
|
+
| `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### Waveform
|
|
219
|
+
|
|
220
|
+
A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). All operations return a new `Waveform`.
|
|
221
|
+
|
|
222
|
+
**Arithmetic & comparison**: `+`, `-`, `*`, `//`, `%`, `**`, `/`, `&`, `|`, `^`, `~`, `==`, `!=`, `<<`, `>>`
|
|
223
|
+
|
|
224
|
+
**Filtering & slicing**
|
|
225
|
+
|
|
226
|
+
| Method | Description |
|
|
227
|
+
|--------|-------------|
|
|
228
|
+
| `wave.mask(mask)` | Keep samples where a boolean Waveform or array is True |
|
|
229
|
+
| `wave.filter(fn)` | Keep samples where `fn(value)` is True |
|
|
230
|
+
| `wave.cycle_slice(begin, end)` | Trim to clock cycle range `[begin, end)` |
|
|
231
|
+
| `wave.time_slice(begin, end)` | Trim to simulation time range |
|
|
232
|
+
| `wave.slice(begin_idx, end_idx)` | Trim by array index |
|
|
233
|
+
| `wave.take(indices)` | Select samples at given indices |
|
|
234
|
+
|
|
235
|
+
**Transformation**
|
|
236
|
+
|
|
237
|
+
| Method | Description |
|
|
238
|
+
|--------|-------------|
|
|
239
|
+
| `wave.map(fn, width, signed)` | Element-wise transform |
|
|
240
|
+
| `wave.unique_consecutive()` | Remove consecutive duplicates |
|
|
241
|
+
| `wave.downsample(chunk, fn)` | Aggregate into chunks |
|
|
242
|
+
| `wave.as_signed()` / `wave.as_unsigned()` | Reinterpret signedness |
|
|
243
|
+
|
|
244
|
+
**Bit manipulation**
|
|
245
|
+
|
|
246
|
+
| Method / Syntax | Description |
|
|
247
|
+
|-----------------|-------------|
|
|
248
|
+
| `wave[high:low]` | Extract bit field (Verilog convention, returns unsigned) |
|
|
249
|
+
| `wave[n]` | Extract single bit |
|
|
250
|
+
| `wave.split_bits(n)` | Split into n-bit groups (LSB first) |
|
|
251
|
+
| `Waveform.concatenate([w0, w1, ...])` | Concatenate (w0 = LSB) |
|
|
252
|
+
| `wave.bit_count()` | Population count |
|
|
253
|
+
|
|
254
|
+
**Edge detection** (1-bit only)
|
|
255
|
+
|
|
256
|
+
| Method | Description |
|
|
257
|
+
|--------|-------------|
|
|
258
|
+
| `wave.rising_edge()` | True at 0→1 transitions |
|
|
259
|
+
| `wave.falling_edge()` | True at 1→0 transitions |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### Pattern
|
|
264
|
+
|
|
265
|
+
| Method | Description |
|
|
266
|
+
|--------|-------------|
|
|
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. |
|
|
270
|
+
| `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
|
|
271
|
+
| `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
|
|
272
|
+
| `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
|
|
273
|
+
| `.branch(cond, true_body, false_body)` | Conditional branch. |
|
|
274
|
+
| `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
|
|
275
|
+
| `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
|
|
276
|
+
|
|
277
|
+
**`MatchResult`**
|
|
278
|
+
|
|
279
|
+
| Field | Description |
|
|
280
|
+
|-------|-------------|
|
|
281
|
+
| `.start` / `.end` | Start and end cycle of each match (both inclusive). |
|
|
282
|
+
| `.duration` | `end - start + 1` cycles. |
|
|
283
|
+
| `.status` | `MatchStatus.OK`, `TIMEOUT`, or `REQUIRE_VIOLATED`. |
|
|
284
|
+
| `.captures` | `dict[str, Waveform]` of captured values. |
|
|
285
|
+
| `.filter_valid()` | Return only `OK` matches. |
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 🛠️ Development
|
|
290
|
+
|
|
291
|
+
This project uses [Poetry](https://python-poetry.org/) for dependency management and packaging.
|
|
292
|
+
|
|
293
|
+
### Setup
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
git clone https://github.com/cxzzzz/wavekit.git
|
|
297
|
+
cd wavekit
|
|
298
|
+
poetry install
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Testing
|
|
302
|
+
|
|
303
|
+
Tests are located in the `tests/` directory and run with [pytest](https://pytest.org/).
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# Run all tests
|
|
307
|
+
poetry run pytest
|
|
308
|
+
|
|
309
|
+
# Run a specific test file
|
|
310
|
+
poetry run pytest tests/test_pattern.py
|
|
311
|
+
|
|
312
|
+
# Run with verbose output
|
|
313
|
+
poetry run pytest -v
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Linting & Formatting
|
|
317
|
+
|
|
318
|
+
This project uses [Ruff](https://github.com/astral-sh/ruff) for linting and formatting.
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# Check for lint errors
|
|
322
|
+
poetry run ruff check .
|
|
323
|
+
|
|
324
|
+
# Check formatting (no changes)
|
|
325
|
+
poetry run ruff format --check .
|
|
326
|
+
|
|
327
|
+
# Auto-fix formatting
|
|
328
|
+
poetry run ruff format .
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Type Checking
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
poetry run mypy .
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## 🤝 Contributing
|
|
338
|
+
|
|
339
|
+
Contributions are welcome! Please open an issue to discuss a bug or feature request before submitting a pull request. When contributing code, make sure all tests pass and the linter reports no errors:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
poetry run pytest
|
|
343
|
+
poetry run ruff check .
|
|
344
|
+
poetry run ruff format --check .
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## 📄 License
|
|
348
|
+
|
|
349
|
+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
|
|
350
|
+
|
wavekit-0.5.0/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# wavekit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
|
|
4
|
+
[](https://pypi.org/project/wavekit/)
|
|
5
|
+
[](https://pypi.org/project/wavekit/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
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
|
+
|
|
10
|
+
## ✨ Features
|
|
11
|
+
|
|
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.
|
|
13
|
+
- **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
|
+
- **Pattern Matching**: NFA-based temporal pattern engine that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
|
|
15
|
+
|
|
16
|
+
## 📦 Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install wavekit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Note**: To read FSDB files, ensure the `VERDI_HOME` environment variable is set before installation.
|
|
23
|
+
|
|
24
|
+
## 🚀 Quick Start
|
|
25
|
+
|
|
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.
|
|
27
|
+
|
|
28
|
+
### 1. Batch Signal Extraction
|
|
29
|
+
|
|
30
|
+
Use brace expansion or regular expressions to load multiple related signals in one call.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from wavekit import VcdReader
|
|
34
|
+
|
|
35
|
+
with VcdReader("jtag.vcd") as f:
|
|
36
|
+
# Brace expansion: load J_state and J_next in one call
|
|
37
|
+
# Returns: { ('state',): Waveform, ('next',): Waveform }
|
|
38
|
+
waves = f.load_matched_waveforms(
|
|
39
|
+
"tb.u0.J_{state,next}[3:0]",
|
|
40
|
+
clock_pattern="tb.tck",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Regex mode (@ prefix): capture groups become dict keys
|
|
44
|
+
waves = f.load_matched_waveforms(
|
|
45
|
+
r"tb.u0.@J_([a-z]+)",
|
|
46
|
+
clock_pattern="tb.tck",
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### 2. Signal Analysis
|
|
53
|
+
|
|
54
|
+
Waveforms support Numpy-style arithmetic, masking, and edge detection out of the box.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import numpy as np
|
|
58
|
+
from wavekit import VcdReader
|
|
59
|
+
|
|
60
|
+
with VcdReader("fifo_tb.vcd") as f:
|
|
61
|
+
clock = "fifo_tb.clk"
|
|
62
|
+
depth = 8
|
|
63
|
+
|
|
64
|
+
w_ptr = f.load_waveform("fifo_tb.s_fifo.w_ptr[2:0]", clock=clock)
|
|
65
|
+
r_ptr = f.load_waveform("fifo_tb.s_fifo.r_ptr[2:0]", clock=clock)
|
|
66
|
+
wr_en = f.load_waveform("fifo_tb.s_fifo.wr_en", clock=clock)
|
|
67
|
+
|
|
68
|
+
occupancy = (w_ptr + depth - r_ptr) % depth
|
|
69
|
+
print(f"Average occupancy: {np.mean(occupancy.value):.2f}")
|
|
70
|
+
|
|
71
|
+
# Filter to cycles where a write is active
|
|
72
|
+
write_occ = occupancy.mask(wr_en == 1)
|
|
73
|
+
|
|
74
|
+
# Detect write bursts
|
|
75
|
+
burst_cycles = wr_en.rising_edge()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### 3. Expression Evaluation
|
|
81
|
+
|
|
82
|
+
Compute waveform expressions directly from signal path strings without loading each signal manually.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from wavekit import VcdReader
|
|
86
|
+
|
|
87
|
+
with VcdReader("fifo_tb.vcd") as f:
|
|
88
|
+
# Single mode: paths must each match exactly one signal
|
|
89
|
+
occupancy = f.eval(
|
|
90
|
+
"fifo_tb.s_fifo.w_ptr[2:0] - fifo_tb.s_fifo.r_ptr[2:0]",
|
|
91
|
+
clock="fifo_tb.clk",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Zip mode: brace patterns expand per key, evaluated once per match
|
|
95
|
+
# Returns: { (0,): Waveform, (1,): Waveform, (2,): Waveform, (3,): Waveform }
|
|
96
|
+
occupancies = f.eval(
|
|
97
|
+
"tb.fifo_{0..3}.w_ptr[2:0] - tb.fifo_{0..3}.r_ptr[2:0]",
|
|
98
|
+
clock="tb.clk",
|
|
99
|
+
mode="zip",
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 4. Pattern Matching
|
|
106
|
+
|
|
107
|
+
Describe a temporal sequence of events; the engine finds all matching transactions in one pass.
|
|
108
|
+
|
|
109
|
+
**AXI-lite Read Latency**
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from wavekit import VcdReader, Pattern
|
|
113
|
+
|
|
114
|
+
with VcdReader("axi_tb.vcd") as f:
|
|
115
|
+
clk = "tb.clk"
|
|
116
|
+
arvalid = f.load_waveform("tb.dut.arvalid", clock=clk)
|
|
117
|
+
arready = f.load_waveform("tb.dut.arready", clock=clk)
|
|
118
|
+
rvalid = f.load_waveform("tb.dut.rvalid", clock=clk)
|
|
119
|
+
rready = f.load_waveform("tb.dut.rready", clock=clk)
|
|
120
|
+
rdata = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)
|
|
121
|
+
|
|
122
|
+
result = (
|
|
123
|
+
Pattern()
|
|
124
|
+
.wait(arvalid & arready) # AR handshake → start
|
|
125
|
+
.wait(rvalid & rready) # R handshake → end
|
|
126
|
+
.capture("rdata", rdata)
|
|
127
|
+
.timeout(256)
|
|
128
|
+
.match()
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
valid = result.filter_valid()
|
|
132
|
+
print(f"Read latencies (cycles): {valid.duration.value}")
|
|
133
|
+
print(f"Read data: {valid.captures['rdata'].value}")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**AXI Write Burst (multi-beat)**
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
beat = Pattern().wait(wvalid & wready).capture("beats[]", wdata)
|
|
140
|
+
|
|
141
|
+
result = (
|
|
142
|
+
Pattern()
|
|
143
|
+
.wait(awvalid & awready) # AW handshake → burst start
|
|
144
|
+
.loop(beat, until=wlast) # collect beats until wlast
|
|
145
|
+
.timeout(512)
|
|
146
|
+
.match()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
for i, inst in enumerate(result.filter_valid()):
|
|
150
|
+
print(f"Burst {i}: {len(inst.captures['beats'])} beats")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Stall Detection**
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
stall = valid & (ready == 0)
|
|
157
|
+
|
|
158
|
+
result = (
|
|
159
|
+
Pattern()
|
|
160
|
+
.wait(stall.rising_edge()) # stall begins
|
|
161
|
+
.loop(Pattern().delay(1), when=stall) # wait while stalling
|
|
162
|
+
.match()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
stalls = result.filter_valid()
|
|
166
|
+
print(f"Stall durations: {stalls.duration.value} cycles")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 📖 API Reference
|
|
172
|
+
|
|
173
|
+
### Reader
|
|
174
|
+
|
|
175
|
+
| Method | Description |
|
|
176
|
+
|--------|-------------|
|
|
177
|
+
| `VcdReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires `VERDI_HOME`. |
|
|
178
|
+
| `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
|
|
179
|
+
| `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
|
|
180
|
+
| `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
|
|
181
|
+
| `reader.get_matched_signals(pattern)` | Resolve a pattern to signal paths without loading data. |
|
|
182
|
+
| `reader.top_scope_list()` | Return root `Scope` nodes of the signal hierarchy. |
|
|
183
|
+
|
|
184
|
+
**Pattern syntax** used in signal paths:
|
|
185
|
+
|
|
186
|
+
| Syntax | Example | Effect |
|
|
187
|
+
|--------|---------|--------|
|
|
188
|
+
| `{a,b,c}` | `sig_{read,write}` | Enumerate named variants |
|
|
189
|
+
| `{N..M}` | `fifo_{0..3}.ptr` | Integer range |
|
|
190
|
+
| `{N..M..step}` | `lane_{0..6..2}` | Stepped range |
|
|
191
|
+
| `@<regex>` | `@([a-z]+)_valid` | Regex with capture groups |
|
|
192
|
+
| `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) |
|
|
193
|
+
| `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Waveform
|
|
198
|
+
|
|
199
|
+
A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). All operations return a new `Waveform`.
|
|
200
|
+
|
|
201
|
+
**Arithmetic & comparison**: `+`, `-`, `*`, `//`, `%`, `**`, `/`, `&`, `|`, `^`, `~`, `==`, `!=`, `<<`, `>>`
|
|
202
|
+
|
|
203
|
+
**Filtering & slicing**
|
|
204
|
+
|
|
205
|
+
| Method | Description |
|
|
206
|
+
|--------|-------------|
|
|
207
|
+
| `wave.mask(mask)` | Keep samples where a boolean Waveform or array is True |
|
|
208
|
+
| `wave.filter(fn)` | Keep samples where `fn(value)` is True |
|
|
209
|
+
| `wave.cycle_slice(begin, end)` | Trim to clock cycle range `[begin, end)` |
|
|
210
|
+
| `wave.time_slice(begin, end)` | Trim to simulation time range |
|
|
211
|
+
| `wave.slice(begin_idx, end_idx)` | Trim by array index |
|
|
212
|
+
| `wave.take(indices)` | Select samples at given indices |
|
|
213
|
+
|
|
214
|
+
**Transformation**
|
|
215
|
+
|
|
216
|
+
| Method | Description |
|
|
217
|
+
|--------|-------------|
|
|
218
|
+
| `wave.map(fn, width, signed)` | Element-wise transform |
|
|
219
|
+
| `wave.unique_consecutive()` | Remove consecutive duplicates |
|
|
220
|
+
| `wave.downsample(chunk, fn)` | Aggregate into chunks |
|
|
221
|
+
| `wave.as_signed()` / `wave.as_unsigned()` | Reinterpret signedness |
|
|
222
|
+
|
|
223
|
+
**Bit manipulation**
|
|
224
|
+
|
|
225
|
+
| Method / Syntax | Description |
|
|
226
|
+
|-----------------|-------------|
|
|
227
|
+
| `wave[high:low]` | Extract bit field (Verilog convention, returns unsigned) |
|
|
228
|
+
| `wave[n]` | Extract single bit |
|
|
229
|
+
| `wave.split_bits(n)` | Split into n-bit groups (LSB first) |
|
|
230
|
+
| `Waveform.concatenate([w0, w1, ...])` | Concatenate (w0 = LSB) |
|
|
231
|
+
| `wave.bit_count()` | Population count |
|
|
232
|
+
|
|
233
|
+
**Edge detection** (1-bit only)
|
|
234
|
+
|
|
235
|
+
| Method | Description |
|
|
236
|
+
|--------|-------------|
|
|
237
|
+
| `wave.rising_edge()` | True at 0→1 transitions |
|
|
238
|
+
| `wave.falling_edge()` | True at 1→0 transitions |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### Pattern
|
|
243
|
+
|
|
244
|
+
| Method | Description |
|
|
245
|
+
|--------|-------------|
|
|
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. |
|
|
249
|
+
| `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
|
|
250
|
+
| `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
|
|
251
|
+
| `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
|
|
252
|
+
| `.branch(cond, true_body, false_body)` | Conditional branch. |
|
|
253
|
+
| `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
|
|
254
|
+
| `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
|
|
255
|
+
|
|
256
|
+
**`MatchResult`**
|
|
257
|
+
|
|
258
|
+
| Field | Description |
|
|
259
|
+
|-------|-------------|
|
|
260
|
+
| `.start` / `.end` | Start and end cycle of each match (both inclusive). |
|
|
261
|
+
| `.duration` | `end - start + 1` cycles. |
|
|
262
|
+
| `.status` | `MatchStatus.OK`, `TIMEOUT`, or `REQUIRE_VIOLATED`. |
|
|
263
|
+
| `.captures` | `dict[str, Waveform]` of captured values. |
|
|
264
|
+
| `.filter_valid()` | Return only `OK` matches. |
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 🛠️ Development
|
|
269
|
+
|
|
270
|
+
This project uses [Poetry](https://python-poetry.org/) for dependency management and packaging.
|
|
271
|
+
|
|
272
|
+
### Setup
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
git clone https://github.com/cxzzzz/wavekit.git
|
|
276
|
+
cd wavekit
|
|
277
|
+
poetry install
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Testing
|
|
281
|
+
|
|
282
|
+
Tests are located in the `tests/` directory and run with [pytest](https://pytest.org/).
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
# Run all tests
|
|
286
|
+
poetry run pytest
|
|
287
|
+
|
|
288
|
+
# Run a specific test file
|
|
289
|
+
poetry run pytest tests/test_pattern.py
|
|
290
|
+
|
|
291
|
+
# Run with verbose output
|
|
292
|
+
poetry run pytest -v
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Linting & Formatting
|
|
296
|
+
|
|
297
|
+
This project uses [Ruff](https://github.com/astral-sh/ruff) for linting and formatting.
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# Check for lint errors
|
|
301
|
+
poetry run ruff check .
|
|
302
|
+
|
|
303
|
+
# Check formatting (no changes)
|
|
304
|
+
poetry run ruff format --check .
|
|
305
|
+
|
|
306
|
+
# Auto-fix formatting
|
|
307
|
+
poetry run ruff format .
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Type Checking
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
poetry run mypy .
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## 🤝 Contributing
|
|
317
|
+
|
|
318
|
+
Contributions are welcome! Please open an issue to discuss a bug or feature request before submitting a pull request. When contributing code, make sure all tests pass and the linter reports no errors:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
poetry run pytest
|
|
322
|
+
poetry run ruff check .
|
|
323
|
+
poetry run ruff format --check .
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## 📄 License
|
|
327
|
+
|
|
328
|
+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
|