wavekit 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavekit
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: a fundamental package for digital circuit waveform analysis
5
5
  License-File: LICENSE
6
6
  Author: cxzzzz
@@ -78,7 +78,7 @@ with VcdReader("jtag.vcd") as f:
78
78
  # Returns: { ('state',): Waveform(...), ('next',): Waveform(...) }
79
79
  waves = f.load_matched_waveforms(
80
80
  "tb.u0.J_{state,next}[3:0]",
81
- clock="tb.tck"
81
+ clock_pattern="tb.tck"
82
82
  )
83
83
 
84
84
  for (suffix,), wave in waves.items():
@@ -96,7 +96,7 @@ with VcdReader("jtag.vcd") as f:
96
96
  regex_pattern = r"tb.u0.@J_([a-z]+)"
97
97
 
98
98
  # Returns: { ('state',): Waveform(...), ('next',): Waveform(...) }
99
- waves = f.load_matched_waveforms(regex_pattern, clock="tb.tck")
99
+ waves = f.load_matched_waveforms(regex_pattern, clock_pattern="tb.tck")
100
100
 
101
101
  for (name_part,), wave in waves.items():
102
102
  print(f"Matched: {name_part}")
@@ -149,6 +149,63 @@ with VcdReader("bus_tb.vcd") as f:
149
149
  print(f"First valid data: {hex(valid_data.value[0])}")
150
150
  ```
151
151
 
152
+ ### Expression Evaluation
153
+
154
+ Use `eval` to compute waveform expressions directly from signal path strings, without manually loading each signal.
155
+
156
+ **Single mode** — all paths match exactly one signal, returns a single `Waveform`:
157
+
158
+ ```python
159
+ from wavekit import VcdReader
160
+
161
+ with VcdReader("fifo_tb.vcd") as f:
162
+ clock = "fifo_tb.clk"
163
+
164
+ # Equivalent to loading w_ptr and r_ptr separately and computing the difference
165
+ occupancy = f.eval(
166
+ "fifo_tb.s_fifo.w_ptr[2:0] - fifo_tb.s_fifo.r_ptr[2:0]",
167
+ clock=clock,
168
+ )
169
+ print(f"Occupancy width: {occupancy.width}")
170
+ ```
171
+
172
+ Bit-slicing on the expression result is also supported:
173
+
174
+ ```python
175
+ # Load a 4-bit signal and slice out the lower 2 bits
176
+ low_bits = f.eval("tb.dut.data[3:0][1:0]", clock=clock)
177
+ assert low_bits.width == 2
178
+ ```
179
+
180
+ **Zip mode** — paths with brace/regex patterns expand to multiple signals; the expression is evaluated once per matched key and a `dict` is returned:
181
+
182
+ ```python
183
+ with VcdReader("multi_fifo_tb.vcd") as f:
184
+ clock = "tb.clk"
185
+
186
+ # Evaluate occupancy for fifo_0, fifo_1, fifo_2, fifo_3 in one call
187
+ # Returns: { (0,): Waveform, (1,): Waveform, (2,): Waveform, (3,): Waveform }
188
+ occupancies = f.eval(
189
+ "tb.fifo_{0..3}.w_ptr[2:0] - tb.fifo_{0..3}.r_ptr[2:0]",
190
+ clock=clock,
191
+ mode="zip",
192
+ )
193
+
194
+ for (idx,), wave in occupancies.items():
195
+ print(f"fifo_{idx} occupancy width: {wave.width}")
196
+ ```
197
+
198
+ Single-match paths in zip mode are **broadcast** — the same waveform is reused across all keys:
199
+
200
+ ```python
201
+ # base_offset matches one signal; it is reused for every fifo index
202
+ adjusted = f.eval(
203
+ "tb.fifo_{0..3}.w_ptr[2:0] - tb.dut.base_offset[2:0]",
204
+ clock=clock,
205
+ mode="zip",
206
+ )
207
+ ```
208
+
152
209
  ## 🛠️ Development
153
210
 
154
211
  This project adheres to strict code quality standards using modern Python tooling:
@@ -57,7 +57,7 @@ with VcdReader("jtag.vcd") as f:
57
57
  # Returns: { ('state',): Waveform(...), ('next',): Waveform(...) }
58
58
  waves = f.load_matched_waveforms(
59
59
  "tb.u0.J_{state,next}[3:0]",
60
- clock="tb.tck"
60
+ clock_pattern="tb.tck"
61
61
  )
62
62
 
63
63
  for (suffix,), wave in waves.items():
@@ -75,7 +75,7 @@ with VcdReader("jtag.vcd") as f:
75
75
  regex_pattern = r"tb.u0.@J_([a-z]+)"
76
76
 
77
77
  # Returns: { ('state',): Waveform(...), ('next',): Waveform(...) }
78
- waves = f.load_matched_waveforms(regex_pattern, clock="tb.tck")
78
+ waves = f.load_matched_waveforms(regex_pattern, clock_pattern="tb.tck")
79
79
 
80
80
  for (name_part,), wave in waves.items():
81
81
  print(f"Matched: {name_part}")
@@ -128,6 +128,63 @@ with VcdReader("bus_tb.vcd") as f:
128
128
  print(f"First valid data: {hex(valid_data.value[0])}")
129
129
  ```
130
130
 
131
+ ### Expression Evaluation
132
+
133
+ Use `eval` to compute waveform expressions directly from signal path strings, without manually loading each signal.
134
+
135
+ **Single mode** — all paths match exactly one signal, returns a single `Waveform`:
136
+
137
+ ```python
138
+ from wavekit import VcdReader
139
+
140
+ with VcdReader("fifo_tb.vcd") as f:
141
+ clock = "fifo_tb.clk"
142
+
143
+ # Equivalent to loading w_ptr and r_ptr separately and computing the difference
144
+ occupancy = f.eval(
145
+ "fifo_tb.s_fifo.w_ptr[2:0] - fifo_tb.s_fifo.r_ptr[2:0]",
146
+ clock=clock,
147
+ )
148
+ print(f"Occupancy width: {occupancy.width}")
149
+ ```
150
+
151
+ Bit-slicing on the expression result is also supported:
152
+
153
+ ```python
154
+ # Load a 4-bit signal and slice out the lower 2 bits
155
+ low_bits = f.eval("tb.dut.data[3:0][1:0]", clock=clock)
156
+ assert low_bits.width == 2
157
+ ```
158
+
159
+ **Zip mode** — paths with brace/regex patterns expand to multiple signals; the expression is evaluated once per matched key and a `dict` is returned:
160
+
161
+ ```python
162
+ with VcdReader("multi_fifo_tb.vcd") as f:
163
+ clock = "tb.clk"
164
+
165
+ # Evaluate occupancy for fifo_0, fifo_1, fifo_2, fifo_3 in one call
166
+ # Returns: { (0,): Waveform, (1,): Waveform, (2,): Waveform, (3,): Waveform }
167
+ occupancies = f.eval(
168
+ "tb.fifo_{0..3}.w_ptr[2:0] - tb.fifo_{0..3}.r_ptr[2:0]",
169
+ clock=clock,
170
+ mode="zip",
171
+ )
172
+
173
+ for (idx,), wave in occupancies.items():
174
+ print(f"fifo_{idx} occupancy width: {wave.width}")
175
+ ```
176
+
177
+ Single-match paths in zip mode are **broadcast** — the same waveform is reused across all keys:
178
+
179
+ ```python
180
+ # base_offset matches one signal; it is reused for every fifo index
181
+ adjusted = f.eval(
182
+ "tb.fifo_{0..3}.w_ptr[2:0] - tb.dut.base_offset[2:0]",
183
+ clock=clock,
184
+ mode="zip",
185
+ )
186
+ ```
187
+
131
188
  ## 🛠️ Development
132
189
 
133
190
  This project adheres to strict code quality standards using modern Python tooling:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wavekit"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "a fundamental package for digital circuit waveform analysis"
5
5
  authors = ["cxzzzz <cxz19961010@outlook.com>"]
6
6
  readme = "README.md"
@@ -19,3 +19,10 @@ from .signal import Signal as Signal
19
19
  from .waveform import Waveform as Waveform
20
20
 
21
21
  __all__ = ['Waveform', 'VcdReader', 'FsdbReader', 'Scope', 'Signal']
22
+
23
+ try:
24
+ from .readers.fsdb.reader import FsdbReader as FsdbReader
25
+
26
+ __all__.append('FsdbReader')
27
+ except ImportError:
28
+ pass
@@ -0,0 +1,281 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from abc import abstractmethod
5
+ from collections.abc import Sequence
6
+ from typing import Any, Literal
7
+
8
+ import numpy as np
9
+
10
+ from ..scope import Scope, traverse_scope
11
+ from ..signal import Signal
12
+ from ..waveform import Waveform
13
+ from .expr_parser import extract_wave_paths
14
+ from .pattern_parser import (
15
+ PatternMap,
16
+ expand_brace_pattern,
17
+ split_by_hierarchy,
18
+ )
19
+ from .value_change import value_change_to_value_array
20
+
21
+
22
+ class Reader:
23
+ def __init__(self):
24
+ pass
25
+
26
+ def __enter__(self):
27
+ return self
28
+
29
+ def __exit__(self, exc_type, exc_val, exc_tb):
30
+ self.close()
31
+ # exception wont be suppressed
32
+ return False
33
+
34
+ @abstractmethod
35
+ def load_waveform(
36
+ self,
37
+ signal: str,
38
+ clock: str,
39
+ xz_value: int = 0,
40
+ signed: bool = False,
41
+ sample_on_posedge: bool = False,
42
+ begin_time: int | None = None,
43
+ end_time: int | None = None,
44
+ ) -> Waveform:
45
+ pass
46
+
47
+ @abstractmethod
48
+ def get_signal_width(self, signal: str) -> int:
49
+ pass
50
+
51
+ @abstractmethod
52
+ def get_signal_range(self, signal: str) -> tuple[int]:
53
+ pass
54
+
55
+ @staticmethod
56
+ def value_change_to_waveform(
57
+ value_change: np.ndarray,
58
+ clock_changes: np.ndarray,
59
+ width: int | None,
60
+ signed: bool,
61
+ sample_on_posedge: bool = False,
62
+ signal: str = '',
63
+ ) -> Waveform:
64
+ value, clock, time = value_change_to_value_array(
65
+ value_change, clock_changes, sample_on_posedge=sample_on_posedge
66
+ )
67
+
68
+ return Waveform(
69
+ value=value,
70
+ clock=clock,
71
+ time=time,
72
+ signal=Signal(signal, width, signed),
73
+ )
74
+
75
+ @abstractmethod
76
+ def top_scope_list(self) -> Sequence[Scope]:
77
+ pass
78
+
79
+ def get_matched_signals(
80
+ self,
81
+ pattern: str,
82
+ ) -> dict[tuple[Any, ...], str]:
83
+ def combine_dict(
84
+ dict1: dict[tuple[Any, ...], str],
85
+ dict2: dict[tuple[Any, ...], str],
86
+ ) -> dict[tuple[Any, ...], str]:
87
+ common_keys = set(dict1.keys()).intersection(dict2.keys())
88
+
89
+ if common_keys:
90
+ signal1s = [dict1[k] for k in common_keys]
91
+ signal2s = [dict2[k] for k in common_keys]
92
+ raise Exception(
93
+ 'found more than one signal with the same keys: '
94
+ f'keys:{list(common_keys)} , signals:{signal1s + signal2s}'
95
+ )
96
+
97
+ return {**dict1, **dict2}
98
+
99
+ pattern = pattern.strip()
100
+ expanded_pattern_list: list[PatternMap] = [
101
+ expand_brace_pattern(p) for p in split_by_hierarchy(pattern)
102
+ ]
103
+
104
+ matched_signals: dict[tuple[Any, ...], str] = {}
105
+ for scope in self.top_scope_list():
106
+ matched_signals = combine_dict(
107
+ matched_signals,
108
+ traverse_scope(scope, expanded_pattern_list),
109
+ )
110
+ return matched_signals
111
+
112
+ def load_matched_waveforms(
113
+ self,
114
+ pattern: str,
115
+ clock_pattern: str,
116
+ xz_value: int = 0,
117
+ signed: bool = False,
118
+ sample_on_posedge: bool = False,
119
+ begin_time: int | None = None,
120
+ end_time: int | None = None,
121
+ ) -> dict[tuple[Any, ...], Waveform]:
122
+ matched_clocks = self.get_matched_signals(clock_pattern)
123
+ if not matched_clocks:
124
+ raise Exception(f'clock pattern {clock_pattern} can not match any signal')
125
+
126
+ matched_signals = self.get_matched_signals(pattern)
127
+
128
+ load_kwargs: dict[str, Any] = dict(
129
+ xz_value=xz_value,
130
+ signed=signed,
131
+ sample_on_posedge=sample_on_posedge,
132
+ begin_time=begin_time,
133
+ end_time=end_time,
134
+ )
135
+
136
+ if len(matched_clocks) == 1:
137
+ # Broadcast: single clock shared by all matched signals
138
+ clock_full_name = next(iter(matched_clocks.values()))
139
+ return {
140
+ k: self.load_waveform(s, clock_full_name, **load_kwargs)
141
+ for k, s in matched_signals.items()
142
+ }
143
+ else:
144
+ # Per-signal clock: keys must match exactly
145
+ if set(matched_clocks.keys()) != set(matched_signals.keys()):
146
+ raise Exception(
147
+ f'clock pattern {clock_pattern!r} matched keys {sorted(matched_clocks.keys())} '
148
+ f'which do not match signal pattern keys {sorted(matched_signals.keys())}'
149
+ )
150
+ return {
151
+ k: self.load_waveform(s, matched_clocks[k], **load_kwargs)
152
+ for k, s in matched_signals.items()
153
+ }
154
+
155
+ @abstractmethod
156
+ def close(self):
157
+ pass
158
+
159
+ # ------------------------------------------------------------------
160
+ # High-level expression APIs
161
+ # ------------------------------------------------------------------
162
+
163
+ def eval(
164
+ self,
165
+ expr: str,
166
+ clock: str,
167
+ xz_value: int = 0,
168
+ signed: bool = False,
169
+ sample_on_posedge: bool = False,
170
+ begin_time: int | None = None,
171
+ end_time: int | None = None,
172
+ mode: Literal['single', 'zip'] = 'single',
173
+ ) -> Waveform | dict[tuple[Any, ...], Waveform]:
174
+ """Evaluate a waveform expression string.
175
+
176
+ Wave signal paths embedded in *expr* are automatically extracted,
177
+ loaded, and substituted before evaluating the resulting Python
178
+ arithmetic expression.
179
+
180
+ Parameters
181
+ ----------
182
+ expr:
183
+ Expression string.
184
+ Single mode example: ``"tb.dut.w_ptr[3:0] - tb.dut.r_ptr[3:0]"``.
185
+ Zip mode example: ``"tb.dut.fifo_{0..3}.w_ptr[3:0] - tb.dut.fifo_{0..3}.r_ptr[3:0]"``.
186
+ clock:
187
+ Clock signal used for all waveform loads.
188
+ mode:
189
+ ``'single'`` (default) — every path must match exactly one
190
+ signal; returns a single :class:`Waveform`.
191
+ ``'zip'`` — paths matching multiple signals must all share the
192
+ same set of pattern keys; single-match paths are broadcast;
193
+ returns ``dict[tuple, Waveform]``.
194
+ """
195
+ substituted, path_entries = extract_wave_paths(expr)
196
+
197
+ load_kwargs: dict[str, Any] = dict(
198
+ clock=clock,
199
+ xz_value=xz_value,
200
+ signed=signed,
201
+ sample_on_posedge=sample_on_posedge,
202
+ begin_time=begin_time,
203
+ end_time=end_time,
204
+ )
205
+
206
+ # Resolve each path to its matched signal(s)
207
+ matched_per_path: list[tuple[str, str, dict[tuple[Any, ...], str]]] = []
208
+ for placeholder, path in path_entries:
209
+ matched = self.get_matched_signals(path)
210
+ if not matched:
211
+ raise ValueError(f"path '{path}' matched no signals")
212
+ matched_per_path.append((placeholder, path, matched))
213
+
214
+ if mode == 'single':
215
+ for _placeholder, path, matched in matched_per_path:
216
+ if len(matched) > 1:
217
+ raise ValueError(
218
+ f"path '{path}' matched {len(matched)} signals in mode='single',"
219
+ f" use mode='zip'. Matched: {list(matched.values())}"
220
+ )
221
+ ns: dict[str, Any] = {
222
+ placeholder: self.load_waveform(next(iter(matched.values())), **load_kwargs)
223
+ for placeholder, _, matched in matched_per_path
224
+ }
225
+ try:
226
+ code = compile(ast.parse(substituted, mode='eval'), '<eval_expr>', 'eval')
227
+ return eval(code, {'__builtins__': {}}, ns) # noqa: S307
228
+ except Exception as exc:
229
+ raise ValueError(
230
+ f"failed to evaluate expression '{expr}' (substituted: '{substituted}')"
231
+ ) from exc
232
+
233
+ elif mode == 'zip':
234
+ multi_paths = [(ph, p, m) for ph, p, m in matched_per_path if len(m) > 1]
235
+ single_paths = [(ph, p, m) for ph, p, m in matched_per_path if len(m) == 1]
236
+
237
+ if multi_paths:
238
+ # All multi-match paths must share identical key sets
239
+ ref_ph, ref_p, ref_matched = multi_paths[0]
240
+ ref_keys = set(ref_matched.keys())
241
+ for _ph, p, matched in multi_paths[1:]:
242
+ if set(matched.keys()) != ref_keys:
243
+ raise ValueError(
244
+ f'inconsistent match keys between paths: '
245
+ f"'{ref_p}' has keys {sorted(ref_keys)}, "
246
+ f"'{p}' has keys {sorted(matched.keys())}"
247
+ )
248
+ zip_keys: list[tuple[Any, ...]] = list(ref_keys)
249
+ else:
250
+ # All paths are single-match; degenerate to single behaviour
251
+ zip_keys = [()]
252
+
253
+ # Pre-load broadcast waveforms (single-match paths)
254
+ broadcast_ns: dict[str, Waveform] = {
255
+ placeholder: self.load_waveform(next(iter(matched.values())), **load_kwargs)
256
+ for placeholder, _, matched in single_paths
257
+ }
258
+
259
+ result: dict[tuple[Any, ...], Waveform] = {}
260
+ try:
261
+ code = compile(ast.parse(substituted, mode='eval'), '<eval_expr>', 'eval')
262
+ except SyntaxError as exc:
263
+ raise ValueError(
264
+ f"invalid expression '{expr}' (substituted: '{substituted}')"
265
+ ) from exc
266
+
267
+ for key in zip_keys:
268
+ ns = dict(broadcast_ns)
269
+ for placeholder, _, matched in multi_paths:
270
+ ns[placeholder] = self.load_waveform(matched[key], **load_kwargs)
271
+ try:
272
+ result[key] = eval(code, {'__builtins__': {}}, ns) # noqa: S307
273
+ except Exception as exc:
274
+ raise ValueError(
275
+ f"failed to evaluate expression '{expr}' for key {key}"
276
+ ) from exc
277
+
278
+ return result
279
+
280
+ else:
281
+ raise ValueError(f"unknown mode '{mode}', expected 'single' or 'zip'")
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ # Matches a waveform signal path token in an expression.
6
+ # A valid path is one of:
7
+ # - Starts with $$ or $ (module search)
8
+ # - Starts with @ (regex pattern)
9
+ # - Contains at least one . separator (hierarchical path)
10
+ #
11
+ # Path structure:
12
+ # [$$|$|@] first_ident (.segment[{brace}])* ([N:M]|[N])?
13
+ #
14
+ # The trailing [N:M] / [N] is consumed as the signal range (not a bit-slice).
15
+ # Any further [N:M] after the placeholder in the resulting expression will be
16
+ # handled as a Python subscript on the Waveform object (i.e. bit-slicing).
17
+ _WAVE_PATH_RE = re.compile(
18
+ r'(?P<path>'
19
+ r'(?:[$]{1,2}|@)?' # optional prefix $$ / $ / @
20
+ r'(?:[A-Za-z_][A-Za-z0-9_]*)' # first identifier segment
21
+ r'(?:[.][A-Za-z_][A-Za-z0-9_{}.,]*)*' # dot-separated segments (allow braces, no [ ])
22
+ r'(?:\[\d+(?::\d+)?\])?' # optional trailing [N:M] or [N] range
23
+ r')'
24
+ )
25
+
26
+
27
+ def _is_wave_path(token: str) -> bool:
28
+ """Return True if token looks like a waveform signal path."""
29
+ if token.startswith('$') or token.startswith('@'):
30
+ return True
31
+ # Must contain a dot (hierarchy) but not look like a floating-point literal
32
+ if '.' in token:
33
+ try:
34
+ float(token)
35
+ return False # it's a numeric literal like "1.0"
36
+ except ValueError:
37
+ return True
38
+ return False
39
+
40
+
41
+ def extract_wave_paths(expr: str) -> tuple[str, list[tuple[str, str]]]:
42
+ """Parse *expr* and replace wave path tokens with placeholder identifiers.
43
+
44
+ Returns
45
+ -------
46
+ substituted_expr : str
47
+ The expression with every wave path replaced by ``__wave_N__``.
48
+ paths : list of (placeholder, original_path)
49
+ The ordered list of substitutions made.
50
+ """
51
+ paths: list[tuple[str, str]] = []
52
+ counter = 0
53
+
54
+ def replacer(m: re.Match) -> str:
55
+ nonlocal counter
56
+ token = m.group('path')
57
+ if not _is_wave_path(token):
58
+ return token
59
+ placeholder = f'__wave_{counter}__'
60
+ paths.append((placeholder, token))
61
+ counter += 1
62
+ return placeholder
63
+
64
+ substituted = _WAVE_PATH_RE.sub(replacer, expr)
65
+ return substituted, paths
@@ -82,8 +82,13 @@ cdef extern from "npi_fsdb.h":
82
82
  npiFsdbScopeFullName
83
83
  npiFsdbScopeDefName
84
84
  npiFsdbScopeType
85
-
86
-
85
+
86
+ ctypedef enum npiFsdbSigCompositeType_e:
87
+ npiFsdbSigCtArray
88
+ npiFsdbSigCtStruct
89
+ npiFsdbSigCtUnion
90
+ npiFsdbSigCtTaggedUnion
91
+ npiFsdbSigCtRecord
87
92
 
88
93
 
89
94
  npiFsdbFileHandle npi_fsdb_open( const NPI_BYTE8* name )
@@ -113,3 +118,5 @@ cdef extern from "npi_fsdb.h":
113
118
  npiFsdbSigHandle npi_fsdb_iter_sig_next( npiFsdbSigIter iter )
114
119
  npiFsdbSigIter npi_fsdb_iter_sig( npiFsdbScopeHandle scope )
115
120
  npiFsdbSigIter npi_fsdb_iter_member( npiFsdbSigHandle sig )
121
+ NPI_INT32 npi_fsdb_iter_scope_stop( npiFsdbScopeIter iter )
122
+ NPI_INT32 npi_fsdb_iter_sig_stop( npiFsdbSigIter iter )
@@ -10,37 +10,53 @@ from libc.stdlib cimport malloc, free
10
10
  from libc.string cimport strcpy, strlen
11
11
  from .npi_fsdb cimport *
12
12
 
13
+
13
14
  cdef class NpiFsdbScope:
15
+ pass
16
+
17
+
18
+ cdef class NpiFsdbNormalScope(NpiFsdbScope):
14
19
  cdef npiFsdbScopeHandle scope_handle
15
20
 
16
21
  @staticmethod
17
22
  cdef init(npiFsdbScopeHandle scope_handle):
18
- scope = NpiFsdbScope()
23
+ scope = NpiFsdbNormalScope()
19
24
  scope.scope_handle = scope_handle
20
25
  return scope
21
26
 
22
27
  def __init__(self):
23
28
  self.scope_handle = NULL
24
29
 
25
- def child_scope_list(self) -> list[NpiFsdbScope]:
30
+ def child_scope_list(self, include_signal_scope:bool = True) -> list[NpiFsdbScope]:
26
31
  assert self.scope_handle != NULL
27
-
28
32
  res = []
29
33
  child_scope_handle_iter = npi_fsdb_iter_child_scope(self.scope_handle)
34
+
30
35
  cdef npiFsdbScopeHandle child_scope_handle
31
36
  while True:
32
37
  child_scope_handle = npi_fsdb_iter_scope_next(child_scope_handle_iter)
33
38
  if child_scope_handle == NULL:
34
39
  break
35
- res.append(NpiFsdbScope.init(child_scope_handle))
36
-
40
+ res.append(NpiFsdbNormalScope.init(child_scope_handle))
41
+ npi_fsdb_iter_scope_stop(child_scope_handle_iter)
42
+
43
+
44
+ cdef npiFsdbSigHandle child_sig_handle
45
+ if include_signal_scope:
46
+ child_sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
47
+ while True:
48
+ child_sig_handle = npi_fsdb_iter_sig_next(child_sig_handle_iter)
49
+ if child_sig_handle == NULL:
50
+ break
51
+ res.append(NpiFsdbSignalScope.init(child_sig_handle))
52
+ npi_fsdb_iter_sig_stop(child_sig_handle_iter)
37
53
  return res
38
54
 
39
55
  def signal_list(self) -> list[str]:
40
56
  assert self.scope_handle != NULL
41
57
 
42
58
  res = []
43
- cdef npiFsdbScopeHandle sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
59
+ sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
44
60
  cdef npiFsdbSigHandle sig_handle
45
61
  cdef const char* c_str
46
62
  while True:
@@ -50,21 +66,92 @@ cdef class NpiFsdbScope:
50
66
  c_str = npi_fsdb_sig_property_str(npiFsdbSigName, sig_handle)
51
67
  name = c_str.decode('ascii')
52
68
  res.append(name)
69
+ npi_fsdb_iter_sig_stop(sig_handle_iter)
53
70
 
54
71
  return res
55
72
 
56
73
  def name(self) -> str:
57
74
  assert self.scope_handle != NULL
58
- return npi_fsdb_scope_property_str(npiFsdbScopeName, self.scope_handle).decode('ascii')
75
+ name = npi_fsdb_scope_property_str(npiFsdbScopeName, self.scope_handle).decode('ascii')
76
+ return name
59
77
 
60
78
  def type(self) -> str:
61
79
  assert self.scope_handle != NULL
62
- return npi_fsdb_scope_property_str(npiFsdbScopeType, self.scope_handle).decode('ascii')
80
+ type_str = npi_fsdb_scope_property_str(npiFsdbScopeType, self.scope_handle).decode('ascii')
81
+ return type_str
63
82
 
64
83
  def def_name(self) -> str:
65
84
  assert self.scope_handle != NULL
66
- return npi_fsdb_scope_property_str(npiFsdbScopeDefName, self.scope_handle).decode('ascii')
85
+ def_name = npi_fsdb_scope_property_str(npiFsdbScopeDefName, self.scope_handle).decode('ascii')
86
+ return def_name
87
+
88
+ cdef class NpiFsdbSignalScope(NpiFsdbScope):
89
+ cdef npiFsdbSigHandle sig_handle
90
+
91
+ @staticmethod
92
+ cdef init(npiFsdbSigHandle sig_handle):
93
+ scope = NpiFsdbSignalScope()
94
+ scope.sig_handle = sig_handle
95
+ return scope
96
+
97
+ def __init__(self):
98
+ self.sig_handle = NULL
99
+
100
+ def child_scope_list(self, include_signal_scope:bool = True) -> list[NpiFsdbScope]:
101
+ assert self.sig_handle != NULL
102
+
103
+ cdef int has_member
104
+ cdef npiFsdbSigIter member_iter
105
+ cdef npiFsdbSigHandle member_handle
106
+
107
+ res = []
108
+ if include_signal_scope:
109
+ if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member:
110
+ member_iter = npi_fsdb_iter_member(self.sig_handle)
111
+ while True:
112
+ member_handle = npi_fsdb_iter_sig_next(member_iter)
113
+ if member_handle == NULL:
114
+ break
115
+ res.append(NpiFsdbSignalScope.init(member_handle))
116
+ npi_fsdb_iter_sig_stop(member_iter)
117
+ return res
118
+
119
+ def signal_list(self) -> list[str]:
120
+ assert self.sig_handle != NULL
121
+
122
+ cdef const char* c_str
123
+ cdef npiFsdbSigIter member_iter
124
+ cdef npiFsdbSigHandle member_handle
125
+ cdef int has_member
126
+ res = []
67
127
 
128
+ if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member:
129
+ member_iter = npi_fsdb_iter_member(self.sig_handle)
130
+ while True:
131
+ member_handle = npi_fsdb_iter_sig_next(member_iter)
132
+ if member_handle == NULL:
133
+ break
134
+ c_str = npi_fsdb_sig_property_str(npiFsdbSigName, member_handle)
135
+ name = c_str.decode('ascii')
136
+ name = name.split(".")
137
+ name = name[len(name)-1]
138
+ res.append(name)
139
+ npi_fsdb_iter_sig_stop(member_iter)
140
+ return res
141
+
142
+ def name(self) -> str:
143
+ assert self.sig_handle != NULL
144
+ name = npi_fsdb_sig_property_str(npiFsdbSigName, self.sig_handle).decode('ascii')
145
+ #TODO: this only works for non-array type (array's name is like data[0],can't be split by ".")
146
+ name = name.split(".")
147
+ name = name[len(name)-1]
148
+ return name
149
+
150
+ def type(self) -> str:
151
+ raise NotImplementedError("type property of signal scope is not supported")
152
+
153
+ def def_name(self) -> str:
154
+ raise NotImplementedError("def_name property of signal scope is not supported")
68
155
  @cython.boundscheck(False) # 关闭边界检查以提升性能
69
156
  @cython.wraparound(False) # 关闭负索引检查以提升性能
70
157
  cdef inline cstr_to_ull(char* str, unsigned int xz_value, int max_bit_num = 2147483647):
@@ -150,7 +237,7 @@ cdef inline fsdb_read_value_change(vct_handle: npiFsdbVctHandle, begin_time, end
150
237
 
151
238
  return result
152
239
 
153
- cdef get_signal_handle_width( npiFsdbSigHandle signal_handle):
240
+ cdef get_signal_handle_width(npiFsdbSigHandle signal_handle):
154
241
  cdef int has_member
155
242
  cdef int width
156
243
  cdef npiFsdbSigIter sub_signal_iter
@@ -167,11 +254,33 @@ cdef get_signal_handle_width( npiFsdbSigHandle signal_handle):
167
254
  width += get_signal_handle_width(sub_signal)
168
255
  return width
169
256
 
257
+ cdef get_signal_handle_range(npiFsdbSigHandle signal_handle):
258
+ cdef int has_member
259
+ cdef int type
260
+ cdef int left_range, right_range
261
+ assert(npi_fsdb_sig_property(npiFsdbSigHasMember, signal_handle, &has_member))
262
+ assert(npi_fsdb_sig_property(npiFsdbSigLeftRange, signal_handle, &left_range))
263
+ assert(npi_fsdb_sig_property(npiFsdbSigRightRange, signal_handle, &right_range))
264
+ return (left_range, right_range)
265
+
266
+ if(has_member != 0):
267
+ assert(npi_fsdb_sig_property(npiFsdbSigCompositeType, signal_handle, &type))
268
+ if(type == <int>npiFsdbSigCtArray):
269
+ return (left_range, right_range)
270
+ return None
271
+
170
272
  cdef class NpiFsdbReader:
171
273
  cdef npiFsdbFileHandle fsdb_handle
172
274
  cdef str file
173
275
 
174
276
  def __init__(self, str file):
277
+ import os
278
+
279
+ # 检查VERDI_HOME环境变量
280
+ verdi_home = os.environ.get('VERDI_HOME')
281
+ if verdi_home:
282
+ npi_lib_path = os.path.join(verdi_home, 'share/NPI/lib/LINUX64/libNPI.so')
283
+
175
284
  #cdef int argc = len(sys.argv)
176
285
  #cdef char** argv = <char**>malloc((argc + 1) * sizeof(char*))
177
286
 
@@ -188,6 +297,8 @@ cdef class NpiFsdbReader:
188
297
  file_str = file.encode('utf-8')
189
298
  cdef char* file_s = file_str
190
299
  self.fsdb_handle = npi_fsdb_open(file_s)
300
+ if(self.fsdb_handle == NULL):
301
+ raise OSError(f"Failed to open fsdb file :{file_str}")
191
302
 
192
303
 
193
304
 
@@ -195,10 +306,22 @@ cdef class NpiFsdbReader:
195
306
  self,
196
307
  str signal
197
308
  ) -> int:
309
+
310
+ cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
311
+ assert signal_handle != NULL, f"can't find signal: {signal}"
312
+
313
+ cdef int width = get_signal_handle_width(signal_handle)
314
+ return width
198
315
 
316
+ def get_signal_range(
317
+ self,
318
+ str signal
319
+ ) -> tuple[int,int]:
320
+
199
321
  cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
200
322
  assert signal_handle != NULL, f"can't find signal: {signal}"
201
- return get_signal_handle_width(signal_handle)
323
+
324
+ return get_signal_handle_range(signal_handle)
202
325
 
203
326
  @cython.boundscheck(False) # 关闭边界检查以提升性能
204
327
  @cython.wraparound(False) # 关闭负索引检查以提升性能
@@ -209,7 +332,7 @@ cdef class NpiFsdbReader:
209
332
  unsigned long long end_time,
210
333
  int xz_value
211
334
  ) -> np.ndarray:
212
-
335
+
213
336
  cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
214
337
  assert signal_handle != NULL, f"can't find signal: {signal}"
215
338
  cdef int width = get_signal_handle_width(signal_handle)
@@ -235,6 +358,7 @@ cdef class NpiFsdbReader:
235
358
  stat = npi_fsdb_goto_time(signal_vct_handle, <npiFsdbTime> begin_time)
236
359
  else:
237
360
  stat = npi_fsdb_goto_next(signal_vct_handle)
361
+
238
362
  if stat == 0:
239
363
  break
240
364
 
@@ -245,6 +369,7 @@ cdef class NpiFsdbReader:
245
369
  if cur_time > end_time:
246
370
  break
247
371
  time_array.push_back(cur_time)
372
+ first = False
248
373
 
249
374
  if width == 1: # opt for clk
250
375
  value_array[0].push_back(cstr_to_bit(<char*>cur_value.value.str, xz_value))
@@ -287,7 +412,8 @@ cdef class NpiFsdbReader:
287
412
  top_scope_handle = npi_fsdb_iter_scope_next(top_scope_iter)
288
413
  if top_scope_handle == NULL:
289
414
  break
290
- res.append(NpiFsdbScope.init(top_scope_handle))
415
+ res.append(NpiFsdbNormalScope.init(top_scope_handle))
416
+ npi_fsdb_iter_scope_stop(top_scope_iter)
291
417
  return res
292
418
 
293
419
  def min_time(self) -> int:
@@ -10,7 +10,7 @@ from ...scope import Scope
10
10
  from ...signal import Signal
11
11
  from ...waveform import Waveform
12
12
  from ..base import Reader
13
- from .npi_fsdb_reader import NpiFsdbReader, NpiFsdbScope
13
+ from .npi_fsdb_reader import NpiFsdbReader, NpiFsdbScope, NpiFsdbSignalScope
14
14
 
15
15
 
16
16
  class FsdbScope(Scope):
@@ -39,6 +39,13 @@ class FsdbScope(Scope):
39
39
  def child_scope_list(self) -> Sequence[Scope]:
40
40
  return [FsdbScope(c, self, self.reader) for c in self.handle.child_scope_list()]
41
41
 
42
+ @cached_property
43
+ def child_normal_scope_list(self) -> Sequence[Scope]:
44
+ return [
45
+ FsdbScope(c, self, self.reader)
46
+ for c in self.handle.child_scope_list(include_signal_scope=False)
47
+ ]
48
+
42
49
  @property
43
50
  def type(self) -> str:
44
51
  if not hasattr(self, '_type'):
@@ -65,8 +72,11 @@ class FsdbScope(Scope):
65
72
  return self.parent_scope.end_time if self.parent_scope else 0
66
73
 
67
74
  def preload_module_scope(self):
75
+ if isinstance(self.handle, NpiFsdbSignalScope):
76
+ return {}
77
+
68
78
  preloaded_module_scope = defaultdict(list)
69
- for c in self.child_scope_list:
79
+ for c in self.child_normal_scope_list:
70
80
  for module_name, module_scope_list in c.preload_module_scope().items():
71
81
  preloaded_module_scope[module_name].extend(module_scope_list)
72
82
 
@@ -141,6 +151,9 @@ class FsdbReader(Reader):
141
151
  def get_signal_width(self, signal: str) -> int:
142
152
  return self.file_handle.get_signal_width(signal)
143
153
 
154
+ def get_signal_range(self, signal: str) -> tuple[int]:
155
+ return self.file_handle.get_signal_range(signal)
156
+
144
157
  @property
145
158
  def begin_time(self) -> str:
146
159
  return self.file_handle.min_time()
@@ -1,142 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import abstractmethod
4
- from collections.abc import Sequence
5
- from typing import Any
6
-
7
- import numpy as np
8
-
9
- from ..scope import Scope, traverse_scope
10
- from ..signal import Signal
11
- from ..waveform import Waveform
12
- from .pattern_parser import (
13
- PatternMap,
14
- expand_brace_pattern,
15
- split_by_hierarchy,
16
- )
17
- from .value_change import value_change_to_value_array
18
-
19
-
20
- class Reader:
21
- def __init__(self):
22
- pass
23
-
24
- def __enter__(self):
25
- return self
26
-
27
- def __exit__(self, exc_type, exc_val, exc_tb):
28
- self.close()
29
- # exception wont be suppressed
30
- return False
31
-
32
- @abstractmethod
33
- def load_waveform(
34
- self,
35
- signal: str,
36
- clock: str,
37
- xz_value: int = 0,
38
- signed: bool = False,
39
- sample_on_posedge: bool = False,
40
- begin_time: int | None = None,
41
- end_time: int | None = None,
42
- ) -> Waveform:
43
- pass
44
-
45
- @abstractmethod
46
- def get_signal_width(self, signal: str) -> int:
47
- pass
48
-
49
- @staticmethod
50
- def value_change_to_waveform(
51
- value_change: np.ndarray,
52
- clock_changes: np.ndarray,
53
- width: int | None,
54
- signed: bool,
55
- sample_on_posedge: bool = False,
56
- signal: str = '',
57
- ) -> Waveform:
58
- value, clock, time = value_change_to_value_array(
59
- value_change, clock_changes, sample_on_posedge=sample_on_posedge
60
- )
61
-
62
- return Waveform(
63
- value=value,
64
- clock=clock,
65
- time=time,
66
- signal=Signal(signal, width, signed),
67
- )
68
-
69
- @abstractmethod
70
- def top_scope_list(self) -> Sequence[Scope]:
71
- pass
72
-
73
- def get_matched_signals(
74
- self,
75
- pattern: str,
76
- ) -> dict[tuple[Any, ...], str]:
77
- def combine_dict(
78
- dict1: dict[tuple[Any, ...], str],
79
- dict2: dict[tuple[Any, ...], str],
80
- ) -> dict[tuple[Any, ...], str]:
81
- common_keys = set(dict1.keys()).intersection(dict2.keys())
82
-
83
- if common_keys:
84
- signal1s = [dict1[k] for k in common_keys]
85
- signal2s = [dict2[k] for k in common_keys]
86
- raise Exception(
87
- 'found more than one signal with the same keys: '
88
- f'keys:{list(common_keys)} , signals:{signal1s + signal2s}'
89
- )
90
-
91
- return {**dict1, **dict2}
92
-
93
- pattern = pattern.strip()
94
- expanded_pattern_list: list[PatternMap] = [
95
- expand_brace_pattern(p) for p in split_by_hierarchy(pattern)
96
- ]
97
-
98
- matched_signals: dict[tuple[Any, ...], str] = {}
99
- for scope in self.top_scope_list():
100
- matched_signals = combine_dict(
101
- matched_signals,
102
- traverse_scope(scope, expanded_pattern_list),
103
- )
104
- return matched_signals
105
-
106
- def load_matched_waveforms(
107
- self,
108
- pattern: str,
109
- clock_pattern: str,
110
- xz_value: int = 0,
111
- signed: bool = False,
112
- sample_on_posedge: bool = False,
113
- begin_time: int | None = None,
114
- end_time: int | None = None,
115
- ) -> dict[tuple[Any, ...], Waveform]:
116
- clock_patterns = self.get_matched_signals(clock_pattern)
117
- if not clock_patterns:
118
- raise Exception(f'clock pattern {clock_pattern} can not match any signal')
119
- if len(clock_patterns) > 1:
120
- raise Exception(
121
- f'clock pattern {clock_pattern} match more than one signal: {clock_patterns}'
122
- )
123
- clock_full_name = next(iter(clock_patterns.values()))
124
-
125
- matched_signals = self.get_matched_signals(pattern)
126
-
127
- return {
128
- k: self.load_waveform(
129
- s,
130
- clock_full_name,
131
- xz_value=xz_value,
132
- signed=signed,
133
- sample_on_posedge=sample_on_posedge,
134
- begin_time=begin_time,
135
- end_time=end_time,
136
- )
137
- for k, s in matched_signals.items()
138
- }
139
-
140
- @abstractmethod
141
- def close(self):
142
- pass
File without changes
File without changes
File without changes
File without changes
File without changes