wavekit 0.7.0a1__tar.gz → 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. wavekit-0.7.2/PKG-INFO +556 -0
  2. wavekit-0.7.2/README.md +532 -0
  3. {wavekit-0.7.0a1 → wavekit-0.7.2}/pyproject.toml +2 -1
  4. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/__init__.py +21 -19
  5. wavekit-0.7.2/src/wavekit/expression.py +165 -0
  6. wavekit-0.7.2/src/wavekit/pattern/__init__.py +23 -0
  7. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/pattern/compiler.py +76 -36
  8. wavekit-0.7.2/src/wavekit/pattern/dsl.py +414 -0
  9. wavekit-0.7.2/src/wavekit/pattern/result.py +243 -0
  10. wavekit-0.7.2/src/wavekit/pattern/runtime.py +587 -0
  11. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/pattern/steps.py +46 -62
  12. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/readers/base.py +286 -340
  13. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +200 -300
  14. wavekit-0.7.2/src/wavekit/readers/fsdb/reader.py +210 -0
  15. wavekit-0.7.2/src/wavekit/readers/fst/reader.py +206 -0
  16. wavekit-0.7.2/src/wavekit/readers/hierarchy.py +368 -0
  17. wavekit-0.7.2/src/wavekit/readers/matcher.py +410 -0
  18. wavekit-0.7.2/src/wavekit/readers/range.py +16 -0
  19. wavekit-0.7.2/src/wavekit/readers/vcd/reader.py +178 -0
  20. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/waveform.py +206 -185
  21. wavekit-0.7.0a1/PKG-INFO +0 -513
  22. wavekit-0.7.0a1/README.md +0 -490
  23. wavekit-0.7.0a1/src/wavekit/pattern/__init__.py +0 -9
  24. wavekit-0.7.0a1/src/wavekit/pattern/dsl.py +0 -314
  25. wavekit-0.7.0a1/src/wavekit/pattern/result.py +0 -112
  26. wavekit-0.7.0a1/src/wavekit/pattern/runtime.py +0 -526
  27. wavekit-0.7.0a1/src/wavekit/readers/fsdb/reader.py +0 -231
  28. wavekit-0.7.0a1/src/wavekit/readers/fst/reader.py +0 -206
  29. wavekit-0.7.0a1/src/wavekit/readers/pattern_parser.py +0 -90
  30. wavekit-0.7.0a1/src/wavekit/readers/vcd/reader.py +0 -161
  31. wavekit-0.7.0a1/src/wavekit/scope.py +0 -317
  32. wavekit-0.7.0a1/src/wavekit/signal.py +0 -87
  33. {wavekit-0.7.0a1 → wavekit-0.7.2}/LICENSE +0 -0
  34. {wavekit-0.7.0a1 → wavekit-0.7.2}/build.py +0 -0
  35. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/pattern/errors.py +0 -0
  36. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/readers/expr_parser.py +0 -0
  37. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
  38. {wavekit-0.7.0a1 → wavekit-0.7.2}/src/wavekit/readers/value_change.pyx +0 -0
wavekit-0.7.2/PKG-INFO ADDED
@@ -0,0 +1,556 @@
1
+ Metadata-Version: 2.4
2
+ Name: wavekit
3
+ Version: 0.7.2
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: lark (>=1.2,<2)
17
+ Requires-Dist: numpy (>=2.0.0,<3.0.0)
18
+ Requires-Dist: pylibfst (>=0.2.1,<0.3.0)
19
+ Requires-Dist: pytest (>=8.2.2,<9.0.0)
20
+ Requires-Dist: vcdvcd (>=2.3.5,<3.0.0)
21
+ Project-URL: Repository, https://github.com/cxzzzz/wavekit
22
+ Description-Content-Type: text/markdown
23
+
24
+ # wavekit
25
+
26
+ [![CI](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml/badge.svg)](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
27
+ [![PyPI version](https://img.shields.io/pypi/v/wavekit.svg)](https://pypi.org/project/wavekit/)
28
+ [![Python Versions](https://img.shields.io/pypi/pyversions/wavekit.svg)](https://pypi.org/project/wavekit/)
29
+ [![Downloads](https://pepy.tech/badge/wavekit)](https://pepy.tech/project/wavekit)
30
+ [![License](https://img.shields.io/github/license/cxzzzz/wavekit.svg)](LICENSE)
31
+
32
+ English | [中文](README_ZH.md)
33
+
34
+ **wavekit** is a high-performance Python library for digital waveform analysis. It loads VCD, FST, and FSDB signals into NumPy-backed `Waveform` objects, and builds on them with efficient signal processing, protocol analysis, and automated verification APIs.
35
+
36
+ > 🤖 **AI integration**: [wavekit-mcp](https://github.com/cxzzzz/wavekit-mcp) provides an MCP server for AI-assisted waveform analysis: load signals and run pattern matching from AI tools without hand-written scripts.
37
+
38
+ ## Features
39
+
40
+ - **Batch signal extraction**: use brace expansion, integer ranges, regex, and wildcards to load related signals in one call.
41
+ - **Rich analysis API**: use NumPy-style arithmetic, masking, bit slicing, edge detection, and time/cycle slicing to build signal queries in a few lines.
42
+ - **Temporal pattern matching**: use declarative and programmable Pattern APIs to extract protocol transactions, measure handshake latency, and detect timeout or hang failures.
43
+ - **High-performance waveform processing**: read VCD, FST, and FSDB files into compact NumPy-backed arrays for fast loading and efficient memory use.
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install wavekit
49
+ ```
50
+
51
+ **Note**: To read FSDB files, the Verdi runtime library (`libNPI.so`) must be available at runtime. Configure via:
52
+ - `WAVEKIT_NPI_LIB`: direct path to `libNPI.so`
53
+ - `VERDI_HOME`: Verdi installation directory (searches `$VERDI_HOME/share/NPI/lib/...`)
54
+ - `LD_LIBRARY_PATH`: system library search path
55
+
56
+ ## Quick start
57
+
58
+ > 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.
59
+
60
+ ### 1. Batch Signal Extraction
61
+
62
+ Use brace expansion or regular expressions to load multiple related signals in one call.
63
+
64
+ ```python
65
+ from wavekit import VcdReader
66
+
67
+ with VcdReader("jtag.vcd") as f:
68
+ # Keys contain BraceCapture objects whose groups are ("state",) / ("next",).
69
+ waves = f.load_matched_waveforms(
70
+ "tb.u0.J_{state,next}[3:0]",
71
+ clock_path="tb.tck",
72
+ )
73
+
74
+ # /regex/ is the canonical regex syntax; groups are stored by RegexCapture.
75
+ waves = f.load_matched_waveforms(
76
+ r"tb.u0./J_([a-z]+)/",
77
+ clock_path="tb.tck",
78
+ )
79
+ ```
80
+
81
+ ---
82
+
83
+ ### 2. Signal Analysis
84
+
85
+ Waveforms support NumPy-style arithmetic, masking, and edge detection.
86
+
87
+ ```python
88
+ import numpy as np
89
+ from wavekit import VcdReader
90
+
91
+ with VcdReader("fifo_tb.vcd") as f:
92
+ clock = "fifo_tb.clk"
93
+ depth = 8
94
+
95
+ w_ptr = f.load_waveform("fifo_tb.s_fifo.w_ptr[2:0]", clock=clock)
96
+ r_ptr = f.load_waveform("fifo_tb.s_fifo.r_ptr[2:0]", clock=clock)
97
+ wr_en = f.load_waveform("fifo_tb.s_fifo.wr_en", clock=clock)
98
+
99
+ occupancy = (w_ptr + depth - r_ptr) % depth
100
+ print(f"Average occupancy: {np.mean(occupancy.value):.2f}")
101
+
102
+ # Filter to cycles where a write is active
103
+ write_occ = occupancy.mask(wr_en == 1)
104
+
105
+ # Detect write bursts
106
+ burst_cycles = wr_en.rising_edge()
107
+ ```
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
+
128
+ ---
129
+
130
+ ### 3. Expression Evaluation
131
+
132
+ Compute waveform expressions directly from signal path strings without loading each signal manually.
133
+
134
+ ```python
135
+ from wavekit import VcdReader
136
+
137
+ with VcdReader("fifo_tb.vcd") as f:
138
+ # Single mode: paths must each match exactly one signal
139
+ occupancy = f.eval(
140
+ "fifo_tb.s_fifo.w_ptr[2:0] - fifo_tb.s_fifo.r_ptr[2:0]",
141
+ clock="fifo_tb.clk",
142
+ )
143
+
144
+ # Zip mode: brace patterns expand per typed CaptureKey and evaluate per match.
145
+ occupancies = f.eval(
146
+ "tb.fifo_{0..3}.w_ptr[2:0] - tb.fifo_{0..3}.r_ptr[2:0]",
147
+ clock="tb.clk",
148
+ mode="zip",
149
+ )
150
+ ```
151
+
152
+ ---
153
+
154
+ ### 4. Pattern Matching
155
+
156
+ `Pattern` scans a waveform and extracts matching transactions, such as a request/response pair, a burst, a stall interval, or another repeating timing pattern.
157
+
158
+ Choose the form that matches the transaction shape:
159
+
160
+ - **Declarative**: use `.wait()`, `.consume()`, `.capture()`, and `.loop()` steps to describe fixed flows.
161
+ - **Programmable**: use a normal Python function when the flow depends on waveform values, such as dynamic branches or per-ID routing.
162
+
163
+ In declarative patterns, the first blocking step selects transaction start cycles; later blocking steps describe timing within each matched transaction.
164
+
165
+ `match(pattern)` returns `MatchRecords`, while `collect(body)` returns a Python `list` of extracted values.
166
+
167
+ #### Declarative examples
168
+
169
+ **AXI-Lite read latency**
170
+
171
+ ```python
172
+ from wavekit import VcdReader
173
+ from wavekit.pattern import Pattern, match, collect
174
+
175
+ with VcdReader("axi_tb.vcd") as f:
176
+ clk = "tb.clk"
177
+ arvalid = f.load_waveform("tb.dut.arvalid", clock=clk)
178
+ arready = f.load_waveform("tb.dut.arready", clock=clk)
179
+ rvalid = f.load_waveform("tb.dut.rvalid", clock=clk)
180
+ rready = f.load_waveform("tb.dut.rready", clock=clk)
181
+ rdata = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)
182
+
183
+ pattern = (
184
+ Pattern()
185
+ .wait(arvalid & arready) # AR handshake → transaction starts
186
+ .wait(rvalid & rready) # R handshake → transaction ends
187
+ .capture("rdata", rdata)
188
+ )
189
+ result = match(pattern)
190
+
191
+ ok = result.filter_ok()
192
+ latency = ok.end.clock - ok.start.clock
193
+ print(f"Read latencies (cycles): {latency}")
194
+ print(f"Read data: {ok.captures['rdata'].value}")
195
+ ```
196
+
197
+ **AXI write burst (multi-beat)**
198
+
199
+ ```python
200
+ beat = Pattern().consume(wvalid & wready, channel="w").capture("beats", wdata, mode="list")
201
+
202
+ pattern = (
203
+ Pattern()
204
+ .wait(awvalid & awready) # AW handshake → burst starts
205
+ .loop(beat, until=wlast) # collect each beat until wlast
206
+ )
207
+ result = match(pattern)
208
+
209
+ for i, inst in enumerate(result.filter_ok()):
210
+ print(f"Burst {i}: {len(inst.captures['beats'])} beats")
211
+ ```
212
+
213
+ **Stall detection**
214
+
215
+ ```python
216
+ stall = valid & (ready == 0)
217
+
218
+ pattern = (
219
+ Pattern()
220
+ .wait(stall.rising_edge()) # stall begins
221
+ .loop(Pattern().delay(1), when=stall) # keep waiting until stall ends
222
+ )
223
+ result = match(pattern)
224
+
225
+ stalls = result.filter_ok()
226
+ stall_cycles = stalls.duration.value - 1
227
+ print(f"Stall durations: {stall_cycles} cycles")
228
+ ```
229
+
230
+ #### Programmable example
231
+
232
+ **DMA-style command stream**
233
+
234
+ Use programmable control flow when a command's opcode changes the following timing shape.
235
+
236
+ ```python
237
+ cmd_fire = cmd_valid & cmd_ready # precompute outside the handler
238
+ w_fire = w_valid & w_ready
239
+ rsp_fire = rsp_valid & rsp_ready
240
+ r_fire = r_valid & r_ready
241
+
242
+ OP_READ = 0
243
+ OP_WRITE = 1
244
+
245
+ def read_dma_cmd(ctx):
246
+ if not ctx.value(cmd_fire):
247
+ return None
248
+
249
+ op = int(ctx.value(cmd_op))
250
+ addr = int(ctx.value(cmd_addr))
251
+ length = int(ctx.value(cmd_len))
252
+
253
+ if op == OP_WRITE:
254
+ data = []
255
+ for _ in range(length):
256
+ ctx.consume(w_fire, channel="wdata")
257
+ data.append(int(ctx.value(w_data)))
258
+
259
+ ctx.consume(rsp_fire, channel="rsp")
260
+ return {
261
+ "op": "write",
262
+ "addr": addr,
263
+ "data": data,
264
+ "status": int(ctx.value(rsp_status)),
265
+ }
266
+
267
+ if op == OP_READ:
268
+ ctx.consume(rsp_fire, channel="rsp")
269
+ data = []
270
+ for _ in range(length):
271
+ ctx.consume(r_fire, channel="rdata")
272
+ data.append(int(ctx.value(r_data)))
273
+
274
+ return {"op": "read", "addr": addr, "data": data}
275
+
276
+ ctx.require(False, message=f"unknown DMA op {op}")
277
+ return None
278
+
279
+ commands = collect(read_dma_cmd)
280
+ print(f"Captured {len(commands)} commands")
281
+ ```
282
+
283
+ Some tips for programmable patterns:
284
+
285
+ - Precompute fixed waveform expressions, such as `fire = valid & ready`, outside the handler so they are not rebuilt every cycle.
286
+ - Start the handler with `if ctx.value(fire): ...` to test whether the current cycle starts a transaction, and `return None` otherwise.
287
+ - Use `ctx.try_consume(...)` for non-blocking polling or arbitration between candidate channels. For a linear burst, `ctx.consume(...)` is more direct.
288
+ - Add `timeout=<cycles>` only when a blocking step needs a wait bound. In `match()`, timeout becomes `MatchStatus.Timeout(...)`; in `collect()`, it raises `PatternError`.
289
+
290
+ ---
291
+
292
+ ## API reference
293
+
294
+ ### Reader
295
+
296
+ | Method | Description |
297
+ |--------|-------------|
298
+ | `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file, *, quiet=True)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`) and suppresses the NPI startup banner by default; pass `quiet=False` to keep it. |
299
+ | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
300
+ | `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
301
+ | `reader.load_matched_waveforms(signal_path, clock_path, ...)` | Batch-load matching signals. Returns `dict[CaptureKey, Waveform]`. |
302
+ | `reader.load_matched_unknown_masks(signal_path, clock_path, ...)` | **Experimental.** Batch-load X/Z masks for matched signals. Returns `dict[CaptureKey, Waveform]`. |
303
+ | `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
304
+ | `reader.get_matched_signals(path)` | Resolve a query to `Signal` objects without loading data. |
305
+ | `reader.get_matched_scopes(path)` | Resolve a query to `Scope` objects. |
306
+ | `reader.top_scopes` | Immutable tuple of root `Scope` nodes. |
307
+ | `has_fsdb_support()` | Report whether the Verdi FSDB runtime is currently available. |
308
+
309
+ ### Signal path patterns
310
+
311
+ | Syntax | Example | Effect | Capture in result key |
312
+ |--------|---------|--------|-----------------------|
313
+ | Plain name | `tb.dut.valid` | Exact-name match | None |
314
+ | `{a,b,c}` | `sig_{read,write}` | Enumerate named variants | `BraceCapture(path=..., groups=...)` |
315
+ | `{N..M}` | `fifo_{0..3}.ptr` | Integer range | `BraceCapture(path=..., groups=...)` |
316
+ | `{N..M..step}` | `lane_{0..6..2}` | Stepped range | `BraceCapture(path=..., groups=...)` |
317
+ | `/<regex>/` | `/([a-z]+)_valid/` | Canonical regex syntax with capture groups | `RegexCapture(path=..., groups=...)` |
318
+ | `@<regex>` | `@([a-z]+)_valid` | Legacy-compatible regex syntax | `RegexCapture(path=..., groups=...)` |
319
+ | `*` / `**` | `tb.*.valid` / `tb.**.valid` | Single-level / recursive wildcard | `WildcardCapture(path=...)` |
320
+ | `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
321
+ | `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
322
+
323
+ `$` and `$$` are path-step modifiers that can combine with exact-name, brace,
324
+ and regex matchers. For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
325
+
326
+ Matched-reader and hierarchy query APIs return dictionaries keyed by
327
+ `CaptureKey = tuple[Capture, ...]`. Exact-name components are omitted, so a
328
+ fully exact query uses key `()`.
329
+
330
+ ---
331
+
332
+ ### Waveform
333
+
334
+ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). All operations return a new `Waveform`.
335
+
336
+ **Arithmetic & comparison**: `+`, `-`, `*`, `//`, `%`, `**`, `/`, `&`, `|`, `^`, `~`, `==`, `!=`, `<<`, `>>`
337
+
338
+ **Filtering & slicing**
339
+
340
+ | Method | Description |
341
+ |--------|-------------|
342
+ | `wave.mask(mask)` | Keep samples where a boolean Waveform or array is True |
343
+ | `wave.filter(fn)` | Keep samples where `fn(value)` is True |
344
+ | `wave.cycle_slice(begin, end)` | Trim to clock cycle range `[begin, end)` |
345
+ | `wave.time_slice(begin, end)` | Trim to simulation time range |
346
+ | `wave.slice(begin_idx, end_idx)` | Trim by array index |
347
+ | `wave.take(indices)` | Select samples at given indices |
348
+
349
+ **Transformation**
350
+
351
+ | Method | Description |
352
+ |--------|-------------|
353
+ | `wave.map(fn, width, signed)` | Element-wise transform |
354
+ | `wave.unique_consecutive()` | Remove consecutive duplicate values |
355
+ | `wave.compress()` | Compact a waveform while preserving value changes and the final sample |
356
+ | `wave.downsample(chunk, fn)` | Aggregate into chunks |
357
+ | `wave.as_signed()` / `wave.as_unsigned()` | Reinterpret signedness |
358
+
359
+ **Bit manipulation**
360
+
361
+ | Method / Syntax | Description |
362
+ |-----------------|-------------|
363
+ | `wave[high:low]` | Extract bit field (Verilog convention, returns unsigned) |
364
+ | `wave[n]` | Extract single bit |
365
+ | `wave.split_bits(n)` | Split into n-bit groups (LSB first) |
366
+ | `Waveform.concatenate([w0, w1, ...])` | Concatenate (w0 = LSB) |
367
+ | `wave.bit_count()` | Population count |
368
+
369
+ **Edge detection** (1-bit only)
370
+
371
+ | Method | Description |
372
+ |--------|-------------|
373
+ | `wave.rising_edge()` | True at 0→1 transitions |
374
+ | `wave.falling_edge()` | True at 1→0 transitions |
375
+
376
+ **Relative time access**
377
+
378
+ | Method | Description |
379
+ |--------|-------------|
380
+ | `wave.relative(offset, pad, pad_value)` | Shift by *offset* cycles (positive = future, negative = past) |
381
+ | `wave.ahead(n, pad, pad_value)` | Look *n* cycles into the future (shorthand for `relative(n)`) |
382
+ | `wave.back(n, pad, pad_value)` | Look *n* cycles into the past (shorthand for `relative(-n)`) |
383
+
384
+ `pad` controls boundary handling: `'repeat'` (default) pads with the first/last value, `'value'` pads with a given `pad_value`.
385
+
386
+ ```python
387
+ # Rising edge detection
388
+ rising = (wave == 0) & wave.ahead()
389
+
390
+ # Compare current vs 3 cycles ago
391
+ changed = wave != wave.back(3)
392
+ ```
393
+
394
+ ---
395
+
396
+ ### Pattern
397
+
398
+ **Construction**
399
+
400
+ | API | Description |
401
+ |-----|-------------|
402
+ | `Pattern()` | Create a declarative Pattern. Add steps with builder methods; execution options live on `match()` / `collect()`. |
403
+ | `match(pattern_or_body, *, axis=None, timeout=None, timeout_message=None, start_cycle=None, end_cycle=None)` | Run a declarative Pattern or programmable check body and return `MatchRecords`. Check bodies return `ctx.OK` or `None`. |
404
+ | `collect(body, *, axis=None, timeout=None, timeout_message=None, start_cycle=None, end_cycle=None)` | Run a programmable extraction body and collect each non-`None` Python return value. |
405
+
406
+ **Declarative Steps**
407
+
408
+ | Method | Description |
409
+ |--------|-------------|
410
+ | `.wait(cond, *, require=None, require_message=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 → `MatchStatus.RequireViolated`). |
411
+ | `.consume(cond, channel, *, require=None, require_message=None)` | Block until `cond` is True and this instance can exclusively consume from `channel`. Resumes in the same cycle on success. Use this for request/response pairing and per-key routing. |
412
+ | `.delay(n, *, require=None, require_message=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
413
+ | `.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. |
414
+ | `.require(cond)` | Assert condition; fail with `MatchStatus.RequireViolated` if False. |
415
+ | `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
416
+ | `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
417
+ | `.branch(cond, true_body, false_body)` | Conditional branch. |
418
+
419
+ The same time and ownership operations are available inside Programmable
420
+ patterns as `ctx.wait(...)`, `ctx.consume(...)`, and `ctx.delay(...)`.
421
+
422
+ **Programmable Context**
423
+
424
+ | API | Description |
425
+ |-----|-------------|
426
+ | `ctx.value(waveform, offset=0)` | Read a scalar value at the current sample plus optional offset. |
427
+ | `ctx.cycle(waveform, offset=0)` | Read the cycle number at the current sample plus optional offset. |
428
+ | `ctx.time(waveform, offset=0)` | Read the timestamp at the current sample plus optional offset. |
429
+ | `ctx.wait(cond, require=None, require_message=None)` | Observe cycles until `cond` is true; does not consume the event. |
430
+ | `ctx.consume(cond, channel, require=None, require_message=None)` | Wait for `cond` and exclusively consume from `channel`. |
431
+ | `ctx.try_consume(cond, channel)` | Poll `channel` without blocking. Returns `True` only when both the condition and channel are available. |
432
+ | `ctx.delay(n, require=None, require_message=None)` | Advance `n` cycles. |
433
+ | `ctx.capture(name, value, mode='last')` | Record a capture for programmable `match()`. |
434
+ | `ctx.OK` | Return from programmable `match()` to record a successful match. |
435
+
436
+ **Dynamic callbacks**
437
+
438
+ Callback arguments depend on whether the callback is used in the declarative API or the programmable API:
439
+
440
+ - In declarative APIs such as `Pattern().wait(...)` and `Pattern().consume(...)`, callbacks receive `(index, captures)`.
441
+ - In programmable functions, callbacks passed to methods such as `ctx.wait(...)` and `ctx.consume(...)` take no arguments. If they need the current index, captures, or signal values, close over `ctx`.
442
+
443
+ **Channels and consume vs. wait**
444
+
445
+ `wait()` is observational: every matching instance can see the same event.
446
+ `consume()` adds ownership: only one instance can claim a given `(channel, cycle)`.
447
+ On the same channel, matches with earlier start cycles claim available events first.
448
+
449
+ A `Channel` is an identity token for consume ownership. Pass a `Channel` object,
450
+ a hashable key, or a dynamic callback to `consume(..., channel=...)`. All
451
+ instances sharing the same channel key compete for the same logical channel.
452
+
453
+ ```python
454
+ from collections import defaultdict
455
+ from wavekit.pattern import Channel, Pattern, match
456
+
457
+ # Multi-bank cache: each bank has its own response port, so two banks can return
458
+ # data in the same cycle. Per-bank channels let independent requests consume
459
+ # independent response streams.
460
+ banks = defaultdict(Channel)
461
+
462
+ pattern = (
463
+ Pattern()
464
+ .wait(req_valid)
465
+ .capture('bank', req_addr & 1)
466
+ .consume(
467
+ lambda i, cap: bank_valid[cap['bank']].value[i],
468
+ channel=lambda i, cap: banks[cap['bank']],
469
+ )
470
+ .capture('rdata',
471
+ lambda i, cap: bank_data[cap['bank']].value[i])
472
+ )
473
+ result = match(pattern)
474
+ ```
475
+
476
+ **`MatchRecords`**
477
+
478
+ | Field | Description |
479
+ |-------|-------------|
480
+ | `.start` / `.end` | Point Waveforms. `.value` is the waveform-array sample index, `.clock` is the absolute cycle, and `.time` is the simulation timestamp. End is inclusive. |
481
+ | `.duration` | `end.value - start.value + 1` sampled cycles. |
482
+ | `.status` | `MatchStatus.OK()`, `MatchStatus.Timeout(...)`, or `MatchStatus.RequireViolated(...)`. |
483
+ | `.captures` | `dict[str, Waveform]` of captured values. |
484
+ | `.ok` | Boolean Waveform where `status == MatchStatus.OK()`. |
485
+ | `.failed` | Boolean Waveform where `status != MatchStatus.OK()`. |
486
+ | `.filter_ok()` | Return only `OK` matches. |
487
+ | `.filter_status(status_class)` | Return only matches with the given status class, such as `MatchStatus.Timeout`. |
488
+ | `.filter_failed()` | Return only non-OK matches. |
489
+
490
+ `MatchRecords[i]` returns a single `MatchRecord`, and slices return another
491
+ `MatchRecords` batch.
492
+
493
+ ---
494
+
495
+ ## Development
496
+
497
+ This project uses [Poetry](https://python-poetry.org/) for dependency management and packaging.
498
+
499
+ ### Setup
500
+
501
+ ```bash
502
+ git clone https://github.com/cxzzzz/wavekit.git
503
+ cd wavekit
504
+ poetry install
505
+ ```
506
+
507
+ ### Testing
508
+
509
+ Tests are located in the `tests/` directory and run with [pytest](https://pytest.org/).
510
+
511
+ ```bash
512
+ # Run all tests
513
+ poetry run pytest
514
+
515
+ # Run a specific test file
516
+ poetry run pytest tests/test_pattern.py
517
+
518
+ # Run with verbose output
519
+ poetry run pytest -v
520
+ ```
521
+
522
+ ### Linting & Formatting
523
+
524
+ This project uses [Ruff](https://github.com/astral-sh/ruff) for linting and formatting.
525
+
526
+ ```bash
527
+ # Check for lint errors
528
+ poetry run ruff check .
529
+
530
+ # Check formatting (no changes)
531
+ poetry run ruff format --check .
532
+
533
+ # Auto-fix formatting
534
+ poetry run ruff format .
535
+ ```
536
+
537
+ ### Type Checking
538
+
539
+ ```bash
540
+ poetry run mypy .
541
+ ```
542
+
543
+ ## Contributing
544
+
545
+ Issues and pull requests are welcome. Before sending a pull request, run the tests and format checks:
546
+
547
+ ```bash
548
+ poetry run pytest
549
+ poetry run ruff check .
550
+ poetry run ruff format --check .
551
+ ```
552
+
553
+ ## License
554
+
555
+ This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
556
+