setVCD 0.2.0__py3-none-any.whl

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.
setVCD/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ """SetVCD - Convert VCD signals to sets of time points."""
2
+
3
+ from .exceptions import (
4
+ ClockSignalError,
5
+ EmptyVCDError,
6
+ InvalidInputError,
7
+ InvalidSignalConditionError,
8
+ InvalidTimeRangeError,
9
+ SignalNotFoundError,
10
+ VCDFileNotFoundError,
11
+ VCDParseError,
12
+ VCDSetError,
13
+ )
14
+ from .setVCD import SetVCD
15
+ from .types import (
16
+ FP,
17
+ Raw,
18
+ SignalCondition,
19
+ String,
20
+ Time,
21
+ TimeValue,
22
+ Value,
23
+ ValueType,
24
+ VCDInput,
25
+ )
26
+
27
+ __version__ = "0.2.0"
28
+ __all__ = [
29
+ "SetVCD",
30
+ # Exceptions
31
+ "VCDSetError",
32
+ "VCDFileNotFoundError",
33
+ "VCDParseError",
34
+ "ClockSignalError",
35
+ "SignalNotFoundError",
36
+ "EmptyVCDError",
37
+ "InvalidInputError",
38
+ "InvalidSignalConditionError",
39
+ "InvalidTimeRangeError",
40
+ # Types
41
+ "Time",
42
+ "Value",
43
+ "TimeValue",
44
+ "SignalCondition",
45
+ "VCDInput",
46
+ # ValueTypes
47
+ "ValueType",
48
+ "Raw",
49
+ "String",
50
+ "FP",
51
+ ]
setVCD/exceptions.py ADDED
@@ -0,0 +1,101 @@
1
+ """Exception classes for VCD2Set package."""
2
+
3
+
4
+ class VCDSetError(Exception):
5
+ """Base exception for all VCD2Set errors.
6
+
7
+ All exceptions raised by VCD2Set inherit from this class,
8
+ making it easy to catch all VCD2Set-specific errors.
9
+ """
10
+
11
+ pass
12
+
13
+
14
+ class VCDFileNotFoundError(VCDSetError):
15
+ """Raised when a VCD file cannot be found.
16
+
17
+ This occurs when a filename is passed to VCDSet but the file
18
+ doesn't exist at the specified path.
19
+ """
20
+
21
+ pass
22
+
23
+
24
+ class VCDParseError(VCDSetError):
25
+ """Raised when a VCD file cannot be parsed.
26
+
27
+ This can occur due to:
28
+ - Malformed VCD file
29
+ - Unsupported VCD format
30
+ - vcdvcd library errors during parsing
31
+ - Signal access errors
32
+ """
33
+
34
+ pass
35
+
36
+
37
+ class ClockSignalError(VCDSetError):
38
+ """Raised when clock signal is not found or ambiguous.
39
+
40
+ This occurs when:
41
+ - Specified clock signal doesn't exist in VCD
42
+ - Clock signal name is misspelled
43
+ """
44
+
45
+ pass
46
+
47
+
48
+ class SignalNotFoundError(VCDSetError):
49
+ """Raised when requested signal is not in VCD.
50
+
51
+ This occurs in the get() method when signal_name doesn't
52
+ match any signal in the VCD file.
53
+ """
54
+
55
+ pass
56
+
57
+
58
+ class EmptyVCDError(VCDSetError):
59
+ """Raised when VCD file has no signals or no time data.
60
+
61
+ This can occur when:
62
+ - VCD file is empty or has no signals
63
+ - Clock signal has no time/value pairs
64
+ - VCD has signals but no actual data
65
+ """
66
+
67
+ pass
68
+
69
+
70
+ class InvalidInputError(VCDSetError):
71
+ """Raised when input type is invalid.
72
+
73
+ This occurs when the vcd parameter is neither:
74
+ - A string filename
75
+ - A Path object
76
+ - A vcdvcd.VCDVCD object
77
+ """
78
+
79
+ pass
80
+
81
+
82
+ class InvalidSignalConditionError(VCDSetError):
83
+ """Raised when signal_condition is not callable or raises exception.
84
+
85
+ This occurs when:
86
+ - signal_condition is not a callable object
87
+ - signal_condition raises an exception during evaluation
88
+ """
89
+
90
+ pass
91
+
92
+
93
+ class InvalidTimeRangeError(VCDSetError):
94
+ """Raised when time range is invalid.
95
+
96
+ This occurs when:
97
+ - Clock signal has negative timestamps
98
+ - Time iteration encounters invalid bounds
99
+ """
100
+
101
+ pass
setVCD/py.typed ADDED
@@ -0,0 +1,2 @@
1
+ # PEP 561 marker file
2
+ # This file indicates that the package supports type hints
setVCD/setVCD.py ADDED
@@ -0,0 +1,453 @@
1
+ """SetVCD - Convert VCD signals to sets of time points based on conditions."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional, Set, Tuple, Union
6
+
7
+ import vcdvcd
8
+
9
+ from .exceptions import (
10
+ ClockSignalError,
11
+ EmptyVCDError,
12
+ InvalidInputError,
13
+ InvalidSignalConditionError,
14
+ SignalNotFoundError,
15
+ VCDFileNotFoundError,
16
+ VCDParseError,
17
+ )
18
+ from .types import (
19
+ FP,
20
+ AnyValue,
21
+ FPValue,
22
+ Raw,
23
+ RawValue,
24
+ SignalConditionProtocol,
25
+ SignalProtocol,
26
+ String,
27
+ StringValue,
28
+ Time,
29
+ ValueType,
30
+ VCDInput,
31
+ VCDVCDProtocol,
32
+ )
33
+
34
+
35
+ def _convert_to_int(value: str) -> Optional[int]:
36
+ """Convert vcdvcd binary string to integer (Raw conversion).
37
+
38
+ Args:
39
+ value: Binary string from vcdvcd (e.g., "1010", "xxxx", "z")
40
+
41
+ Returns:
42
+ Integer value for valid binary strings, None for x/z or malformed strings.
43
+
44
+ Examples:
45
+ >>> _convert_to_int("1010") # Binary to decimal
46
+ 10
47
+ >>> _convert_to_int("xxxx") # X/Z values
48
+ None
49
+ """
50
+ # Case-insensitive check for unknown/high-impedance
51
+ value_lower = value.lower()
52
+ if "x" in value_lower or "z" in value_lower:
53
+ return None
54
+
55
+ # Binary to decimal conversion
56
+ try:
57
+ return int(value, 2)
58
+ except ValueError:
59
+ # Defensive: malformed VCD value
60
+ return None
61
+
62
+
63
+ def _convert_to_string(value: str) -> Optional[str]:
64
+ """Convert vcdvcd string to string (String conversion - passthrough)."""
65
+ # Return as-is, only return None for truly invalid input
66
+ if value is None or value == "":
67
+ return None
68
+ return value
69
+
70
+
71
+ def _convert_to_fp(value: str, frac: int, signed: bool) -> Optional[float]:
72
+ """Convert vcdvcd binary string to fixed-point float (FP conversion)."""
73
+ # Validate frac parameter
74
+ if frac < 0:
75
+ raise ValueError(f"frac must be >= 0, got {frac}")
76
+
77
+ # Check for x/z - return NaN per requirements
78
+ value_lower = value.lower()
79
+ if "x" in value_lower or "z" in value_lower:
80
+ return float("nan")
81
+
82
+ try:
83
+ # Convert binary string to integer (unsigned initially)
84
+ int_value = int(value, 2)
85
+ total_bits = len(value)
86
+
87
+ # Handle signed values (two's complement)
88
+ if signed and total_bits > 0:
89
+ sign_bit = 1 << (total_bits - 1)
90
+ if int_value & sign_bit:
91
+ # Negative number in two's complement
92
+ int_value = int_value - (1 << total_bits)
93
+
94
+ # Apply fractional scaling: divide by 2^frac
95
+ float_value = int_value / (1 << frac)
96
+
97
+ return float_value
98
+
99
+ except ValueError:
100
+ # Malformed binary string
101
+ return float("nan")
102
+
103
+
104
+ def _convert_value(value_str: str, value_type: ValueType) -> AnyValue:
105
+ """Dispatch to appropriate converter based on ValueType."""
106
+ if isinstance(value_type, Raw):
107
+ return _convert_to_int(value_str)
108
+ elif isinstance(value_type, String):
109
+ return _convert_to_string(value_str)
110
+ elif isinstance(value_type, FP):
111
+ return _convert_to_fp(value_str, value_type.frac, value_type.signed)
112
+ else:
113
+ # Should never happen with proper typing
114
+ raise ValueError(f"Unknown ValueType: {type(value_type)}")
115
+
116
+
117
+ class SetVCD:
118
+ """
119
+ Query VCD signals with functionally, and combine them with set theory operators.
120
+ """
121
+
122
+ def __init__(self, vcd: VCDInput, clock: str = "clk") -> None:
123
+ """
124
+ Args:
125
+ vcd: Either a filename (str/Path) to a VCD file, or an already-parsed
126
+ vcdvcd.VCDVCD object. If a filename is provided, it will be loaded
127
+ and parsed automatically.
128
+ clock: Exact name of the clock signal to use as time reference. This
129
+ signal's last transition determines the iteration range. Must match
130
+ a signal name exactly (case-sensitive).
131
+ """
132
+ # Load VCD object
133
+ if isinstance(vcd, (str, Path)):
134
+ vcd_path = Path(vcd)
135
+ if not vcd_path.exists():
136
+ raise VCDFileNotFoundError(f"VCD file not found: {vcd_path}")
137
+
138
+ try:
139
+ self.wave: VCDVCDProtocol = vcdvcd.VCDVCD(str(vcd_path))
140
+ except Exception as e:
141
+ raise VCDParseError(f"Failed to parse VCD file: {e}") from e
142
+ elif hasattr(vcd, "get_signals") and hasattr(vcd, "__getitem__"):
143
+ # Duck typing check for VCDVCD-like object
144
+ self.wave = vcd
145
+ else:
146
+ raise InvalidInputError(
147
+ f"vcd must be a filename (str/Path) or VCDVCD object, "
148
+ f"got {type(vcd).__name__}"
149
+ )
150
+
151
+ # Validate VCD has signals
152
+ try:
153
+ all_signals = self.wave.get_signals()
154
+ except Exception as e:
155
+ raise VCDParseError(f"Failed to retrieve signals from VCD: {e}") from e
156
+
157
+ if not all_signals:
158
+ raise EmptyVCDError("VCD file contains no signals")
159
+
160
+ # Initialize signal storage
161
+ self.sigs: Dict[str, SignalProtocol] = {}
162
+
163
+ # Find clock signal - EXACT match only
164
+ if clock not in all_signals:
165
+ # Provide helpful error message with similar signals using fuzzy matching
166
+ # Split the search term into parts and find signals containing those parts
167
+ search_parts = [p for p in clock.lower().split(".") if p]
168
+ similar = []
169
+
170
+ # Score each signal based on how many parts match
171
+ scored_signals = []
172
+ for sig in all_signals:
173
+ sig_lower = sig.lower()
174
+ matches = sum(1 for part in search_parts if part in sig_lower)
175
+ if matches > 0:
176
+ scored_signals.append((matches, sig))
177
+
178
+ # Sort by number of matches (descending) and take top matches
179
+ scored_signals.sort(reverse=True, key=lambda x: x[0])
180
+ similar = [sig for _, sig in scored_signals[:5]]
181
+
182
+ error_msg = f"Clock signal '{clock}' not found in VCD."
183
+ if similar:
184
+ error_msg += f" Did you mean one of: {similar}?"
185
+ else:
186
+ error_msg += f" Available signals: {all_signals[:10]}..."
187
+ raise ClockSignalError(error_msg)
188
+
189
+ try:
190
+ self.sigs["clock"] = self.wave[clock]
191
+ except Exception as e:
192
+ raise VCDParseError(f"Failed to access clock signal '{clock}': {e}") from e
193
+
194
+ # Verify clock has data
195
+ if not self.sigs["clock"].tv:
196
+ raise EmptyVCDError(f"Clock signal '{clock}' has no time/value data")
197
+
198
+ # Get last clock timestamp
199
+ try:
200
+ self.last_clock: Time = self.sigs["clock"].tv[-1][0]
201
+ except (IndexError, TypeError) as e:
202
+ raise EmptyVCDError(
203
+ f"Failed to get last timestamp from clock signal: {e}"
204
+ ) from e
205
+
206
+ def search(self, search_regex: str = "") -> List[str]:
207
+ """Search for signals matching a regex pattern.
208
+
209
+ Args:
210
+ search_regex: Regular expression pattern to match signal names.
211
+ Empty string returns all signals.
212
+
213
+ Returns:
214
+ List of signal names matching the pattern.
215
+
216
+ Example:
217
+ >>> vs = SetVCD("sim.vcd", clock="TOP.clk")
218
+ >>> output_signals = vs.search("output")
219
+ >>> accelerator_signals = vs.search("Accelerator.*valid")
220
+ """
221
+ signals = self.wave.get_signals()
222
+ searched = [s for s in signals if re.search(search_regex, s)]
223
+ return searched
224
+
225
+ def get(
226
+ self,
227
+ signal_name: str,
228
+ signal_condition: Union[
229
+ SignalConditionProtocol[int],
230
+ SignalConditionProtocol[str],
231
+ SignalConditionProtocol[float],
232
+ ],
233
+ value_type: Optional[ValueType] = None,
234
+ ) -> Set[Time]:
235
+ """Filter time points based on signal condition.
236
+
237
+ Args:
238
+ signal_name: Exact name of signal (case-sensitive). Must exist in VCD file.
239
+ signal_condition: Function (sm1, s, sp1) -> bool that evaluates signal values.
240
+ The value types passed depend on value_type parameter:
241
+ - Raw(): receives Optional[int] values
242
+ - String(): receives Optional[str] values
243
+ - FP(): receives Optional[float] values
244
+ value_type: Value conversion type (default: Raw() for backward compatibility).
245
+ - Raw(): Binary to int, x/z become None
246
+ - String(): Keep raw strings including x/z as literals
247
+ - FP(frac, signed): Fixed-point to float, x/z become NaN
248
+
249
+ Returns:
250
+ Set of timesteps where signal_condition returns True.
251
+
252
+ Examples:
253
+ >>> # Integer comparison (default Raw)
254
+ >>> rising = vs.get("clk", lambda sm1, s, sp1: sm1 == 0 and s == 1)
255
+ >>>
256
+ >>> # String matching to detect x/z
257
+ >>> has_x = vs.get("data", lambda sm1, s, sp1: s is not None and 'x' in s,
258
+ ... value_type=String())
259
+ >>>
260
+ >>> # Fixed-point comparison
261
+ >>> above_threshold = vs.get("temp",
262
+ ... lambda sm1, s, sp1: s is not None and s > 25.5,
263
+ ... value_type=FP(frac=8, signed=True))
264
+ """
265
+ # Default to Raw() if not specified
266
+ if value_type is None:
267
+ value_type = Raw()
268
+
269
+ # Validate signal exists
270
+ try:
271
+ all_signals = self.wave.get_signals()
272
+ except Exception as e:
273
+ raise VCDParseError(f"Failed to retrieve signals: {e}") from e
274
+
275
+ if signal_name not in all_signals:
276
+ # Provide helpful error with similar signals using fuzzy matching
277
+ # Split the search term into parts and find signals containing those parts
278
+ search_parts = [p for p in signal_name.lower().split(".") if p]
279
+ similar = []
280
+
281
+ # Score each signal based on how many parts match
282
+ scored_signals = []
283
+ for sig in all_signals:
284
+ sig_lower = sig.lower()
285
+ matches = sum(1 for part in search_parts if part in sig_lower)
286
+ if matches > 0:
287
+ scored_signals.append((matches, sig))
288
+
289
+ # Sort by number of matches (descending) and take top matches
290
+ scored_signals.sort(reverse=True, key=lambda x: x[0])
291
+ similar = [sig for _, sig in scored_signals[:5]]
292
+
293
+ error_msg = f"Signal '{signal_name}' not found in VCD."
294
+ if similar:
295
+ error_msg += f" Did you mean one of: {similar}?"
296
+ else:
297
+ error_msg += f" Available signals: {all_signals[:10]}..."
298
+ raise SignalNotFoundError(error_msg)
299
+
300
+ # Validate signal_condition is callable
301
+ if not callable(signal_condition):
302
+ raise InvalidSignalConditionError(
303
+ f"signal_condition must be callable, got {type(signal_condition).__name__}"
304
+ )
305
+
306
+ # Get signal object
307
+ try:
308
+ signal_obj = self.wave[signal_name]
309
+ except Exception as e:
310
+ raise VCDParseError(f"Failed to access signal '{signal_name}': {e}") from e
311
+
312
+ # Iterate through ALL time steps (not just deltas)
313
+ out: Set[Time] = set()
314
+
315
+ for time in range(0, self.last_clock + 1):
316
+ try:
317
+ # Get raw string values from vcdvcd
318
+ sm1_str: Optional[str] = signal_obj[time - 1] if time > 0 else None
319
+ s_str: str = signal_obj[time]
320
+ sp1_str: Optional[str] = (
321
+ signal_obj[time + 1] if time < self.last_clock else None
322
+ )
323
+
324
+ # Convert values using specified ValueType (None for boundaries)
325
+ sm1: AnyValue = (
326
+ _convert_value(sm1_str, value_type) if sm1_str is not None else None
327
+ )
328
+ s: AnyValue = _convert_value(s_str, value_type)
329
+ sp1: AnyValue = (
330
+ _convert_value(sp1_str, value_type) if sp1_str is not None else None
331
+ )
332
+
333
+ # Evaluate user's condition
334
+ try:
335
+ # Tell pyright to ignore this because we have dependent types.
336
+ check = signal_condition(sm1, s, sp1) # type: ignore[arg-type]
337
+ except Exception as e:
338
+ raise InvalidSignalConditionError(
339
+ f"signal_condition raised exception at time {time}: {e}. "
340
+ f"Note: signal values can be None (for x/z values or boundaries)."
341
+ ) from e
342
+
343
+ # Add time to result set if condition is True
344
+ if check:
345
+ out.add(time)
346
+
347
+ except InvalidSignalConditionError:
348
+ # Re-raise our own exceptions
349
+ raise
350
+ except Exception as e:
351
+ # Wrap any other errors
352
+ raise VCDParseError(
353
+ f"Failed to access signal '{signal_name}' at time {time}: {e}"
354
+ ) from e
355
+
356
+ return out
357
+
358
+ def get_values(
359
+ self,
360
+ signal_name: str,
361
+ timesteps: Set[Time],
362
+ value_type: Optional[ValueType] = None,
363
+ ) -> Union[
364
+ List[Tuple[Time, RawValue]],
365
+ List[Tuple[Time, StringValue]],
366
+ List[Tuple[Time, FPValue]],
367
+ ]:
368
+ """Get signal values at specific timesteps.
369
+
370
+ This method takes a set of timesteps (typically from get()) and returns
371
+ the signal values at those times as a sorted list of (time, value) tuples.
372
+
373
+ Args:
374
+ signal_name: Exact name of the signal to query (case-sensitive).
375
+ Must exist in the VCD file.
376
+ timesteps: Set of integer timesteps to query. Can be empty.
377
+ value_type: Value conversion type (default: Raw() for backward compatibility).
378
+ - Raw(): Binary to int, x/z become None → List[Tuple[Time, Optional[int]]]
379
+ - String(): Keep raw strings including x/z → List[Tuple[Time, Optional[str]]]
380
+ - FP(frac, signed): Fixed-point to float, x/z → NaN → List[Tuple[Time, Optional[float]]]
381
+
382
+ Returns:
383
+ Sorted list of (time, value) tuples. Value type depends on value_type parameter.
384
+
385
+ Examples:
386
+ >>> handshakes = valid_times & ready_times
387
+ >>>
388
+ >>> # Get as integers (default)
389
+ >>> int_values = vs.get_values("counter", handshakes)
390
+ >>>
391
+ >>> # Get as strings to see x/z
392
+ >>> str_values = vs.get_values("data_bus", handshakes, String())
393
+ >>>
394
+ >>> # Get as fixed-point floats
395
+ >>> fp_values = vs.get_values("voltage", handshakes, FP(frac=12, signed=False))
396
+ """
397
+ # Default to Raw() if not specified
398
+ if value_type is None:
399
+ value_type = Raw()
400
+
401
+ # Validate signal exists
402
+ try:
403
+ all_signals = self.wave.get_signals()
404
+ except Exception as e:
405
+ raise VCDParseError(f"Failed to retrieve signals: {e}") from e
406
+
407
+ if signal_name not in all_signals:
408
+ # Provide helpful error with similar signals using fuzzy matching
409
+ search_parts = [p for p in signal_name.lower().split(".") if p]
410
+ similar = []
411
+
412
+ # Score each signal based on how many parts match
413
+ scored_signals = []
414
+ for sig in all_signals:
415
+ sig_lower = sig.lower()
416
+ matches = sum(1 for part in search_parts if part in sig_lower)
417
+ if matches > 0:
418
+ scored_signals.append((matches, sig))
419
+
420
+ # Sort by number of matches (descending) and take top matches
421
+ scored_signals.sort(reverse=True, key=lambda x: x[0])
422
+ similar = [sig for _, sig in scored_signals[:5]]
423
+
424
+ error_msg = f"Signal '{signal_name}' not found in VCD."
425
+ if similar:
426
+ error_msg += f" Did you mean one of: {similar}?"
427
+ else:
428
+ error_msg += f" Available signals: {all_signals[:10]}..."
429
+ raise SignalNotFoundError(error_msg)
430
+
431
+ # Get signal object
432
+ try:
433
+ signal_obj = self.wave[signal_name]
434
+ except Exception as e:
435
+ raise VCDParseError(f"Failed to access signal '{signal_name}': {e}") from e
436
+
437
+ # Get values at each timestep and sort by time
438
+ result: List[Tuple[Time, AnyValue]] = []
439
+ for time in timesteps:
440
+ try:
441
+ value_str: str = signal_obj[time]
442
+ value_converted: AnyValue = _convert_value(value_str, value_type)
443
+ result.append((time, value_converted))
444
+ except Exception as e:
445
+ raise VCDParseError(
446
+ f"Failed to access signal '{signal_name}' at time {time}: {e}"
447
+ ) from e
448
+
449
+ # Sort by time
450
+ result.sort(key=lambda x: x[0])
451
+
452
+ # Tell pyright to ignore this because we have dependent types.
453
+ return result # type: ignore[return-value]
setVCD/types.py ADDED
@@ -0,0 +1,185 @@
1
+ """Type definitions and protocols for setVCD package."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Callable, List, Optional, Protocol, Tuple, TypeVar, Union
6
+
7
+ # Type aliases for clarity and documentation
8
+ Time = int
9
+ """Integer timestamp in VCD time units."""
10
+
11
+ Value = Optional[int]
12
+ """Signal value as integer (binary conversion) or None (for x/z/boundaries)."""
13
+
14
+ TimeValue = Tuple[Time, Value]
15
+ """Tuple of (time, value) representing a signal transition."""
16
+
17
+
18
+ class SignalProtocol(Protocol):
19
+ """Protocol describing the interface of a vcdvcd Signal object.
20
+
21
+ This protocol documents the expected interface without requiring
22
+ an explicit dependency on vcdvcd for type checking.
23
+ """
24
+
25
+ tv: List[TimeValue]
26
+ """List of (time, value) tuples representing signal transitions."""
27
+
28
+ def __getitem__(self, time: Time) -> str:
29
+ """Random access to get signal value at specific time.
30
+
31
+ Returns RAW string value from vcdvcd - will be converted by SetVCD layer.
32
+ Uses binary search to interpolate value at any time point,
33
+ even between transitions.
34
+ """
35
+ ...
36
+
37
+
38
+ class VCDVCDProtocol(Protocol):
39
+ """Protocol describing the interface of a vcdvcd.VCDVCD object.
40
+
41
+ This protocol documents the expected interface without requiring
42
+ an explicit dependency on vcdvcd for type checking.
43
+ """
44
+
45
+ def get_signals(self) -> List[str]:
46
+ """Returns list of all signal names in the VCD file."""
47
+ ...
48
+
49
+ def __getitem__(self, signal_name: str) -> SignalProtocol:
50
+ """Returns Signal object for the given signal name.
51
+
52
+ Args:
53
+ signal_name: Exact signal name (case-sensitive).
54
+
55
+ Returns:
56
+ Signal object with tv attribute and random access.
57
+ """
58
+ ...
59
+
60
+
61
+ SignalCondition = Callable[[Optional[int], Optional[int], Optional[int]], bool]
62
+ """Type for signal condition callbacks.
63
+
64
+ A SignalCondition is a function that takes:
65
+ - sm1: Signal value at time-1 (None if at time 0 or if value is x/z)
66
+ - s: Signal value at current time (None if value is x/z)
67
+ - sp1: Signal value at time+1 (None if at last time or if value is x/z)
68
+
69
+ Returns:
70
+ - True if this time point should be included in the result set
71
+ - False otherwise
72
+
73
+ Examples:
74
+ >>> # Rising edge detector
75
+ >>> rising_edge: SignalCondition = lambda sm1, s, sp1: sm1 == 0 and s == 1
76
+ >>>
77
+ >>> # High level detector
78
+ >>> is_high: SignalCondition = lambda sm1, s, sp1: s == 1
79
+ >>>
80
+ >>> # Change detector
81
+ >>> changed: SignalCondition = lambda sm1, s, sp1: sm1 is not None and sm1 != s
82
+ """
83
+
84
+ VCDInput = Union[str, Path, VCDVCDProtocol]
85
+ """Type for VCD input to SetVCD.
86
+
87
+ Can be:
88
+ - str: Filename path to VCD file
89
+ - Path: Pathlib Path object to VCD file
90
+ - VCDVCDProtocol: Already-parsed vcdvcd.VCDVCD object
91
+ """
92
+
93
+
94
+ # ValueType classes for controlling signal value conversion
95
+ @dataclass(frozen=True)
96
+ class Raw:
97
+ """Represent signal bits as integers (default).
98
+
99
+ X and Z values get turned to None.
100
+
101
+ Example:
102
+ >>> # Default behavior - binary to decimal
103
+ >>> vs.get("data[3:0]", lambda sm1, s, sp1: s == 10, value_type=setvcd.Raw) # "1010" → 10
104
+ """
105
+
106
+ pass
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class String:
111
+ """Represent signal bits as a string.
112
+
113
+ X and Z values stay in the string.
114
+
115
+ Example:
116
+ >>> # Detect x/z values in signal
117
+ >>> vs.get("data", lambda sm1, s, sp1: s is not None and 'x' in s,
118
+ ... value_type=String())
119
+ """
120
+
121
+ pass
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class FP:
126
+ """Represent signal bits as floating point, by assuming its fixed point.
127
+
128
+ X and Z values get turned to None.
129
+
130
+ Args:
131
+ frac: Number of fractional bits (LSBs). Must be >= 0.
132
+ signed: Whether value has a sign bit (MSB in two's complement).
133
+ Default is False (unsigned).
134
+
135
+ Examples:
136
+ >>> # Temperature sensor with 8 fractional bits, unsigned
137
+ >>> vs.get("temp", lambda sm1, s, sp1: s is not None and s > 25.5,
138
+ ... value_type=FP(frac=8, signed=False))
139
+ """
140
+
141
+ frac: int
142
+ signed: bool = False
143
+
144
+
145
+ # Union type for all value types
146
+ ValueType = Union[Raw, String, FP]
147
+ """Union of all ValueType options for signal value conversion."""
148
+
149
+ # Polymorphic value type aliases
150
+ RawValue = Optional[int]
151
+ """Signal value as integer (binary conversion) or None (for x/z/boundaries)."""
152
+
153
+ StringValue = Optional[str]
154
+ """Signal value as string (preserved from vcdvcd) or None (for boundaries)."""
155
+
156
+ FPValue = Optional[float]
157
+ """Signal value as float (fixed-point conversion) or None."""
158
+
159
+ AnyValue = Union[RawValue, StringValue, FPValue]
160
+ """Any signal value type (int, str, or float, or None)."""
161
+
162
+ # Generic type variable for signal conditions
163
+ T = TypeVar("T", int, str, float, contravariant=True)
164
+ """Type variable for signal value types (contravariant for function parameters)."""
165
+
166
+
167
+ class SignalConditionProtocol(Protocol[T]):
168
+ """Protocol for value-type-aware signal condition functions.
169
+
170
+ A generic protocol that accepts signal values of a specific type (int, str, or float)
171
+ and returns a boolean indicating whether the condition is met.
172
+ """
173
+
174
+ def __call__(self, sm1: Optional[T], s: Optional[T], sp1: Optional[T]) -> bool:
175
+ """Evaluate condition on three consecutive signal values.
176
+
177
+ Args:
178
+ sm1: Signal value at time-1 (None if at time 0 or x/z for Raw)
179
+ s: Signal value at current time (None if x/z for Raw)
180
+ sp1: Signal value at time+1 (None if at last time or x/z for Raw)
181
+
182
+ Returns:
183
+ True if this time point should be included in the result set
184
+ """
185
+ ...
@@ -0,0 +1,337 @@
1
+ Metadata-Version: 2.4
2
+ Name: setVCD
3
+ Version: 0.2.0
4
+ Summary: Convert VCD signals to sets of time points based on conditions
5
+ Author-email: Michail Rontionov <mrontionov@mront.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mrontio/setVCD
8
+ Project-URL: Repository, https://github.com/mrontio/setVCD
9
+ Project-URL: Issues, https://github.com/mrontio/setVCD/issues
10
+ Keywords: vcd,verilog,waveform,signal,testing,verification
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Classifier: Topic :: System :: Hardware
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: vcdvcd>=2.0.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.0; extra == "dev"
31
+ Requires-Dist: black>=23.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
33
+ Requires-Dist: isort>=5.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # SetVCD
37
+ Programmatically inspect hardware VCD signals using a high-level functional interface.
38
+
39
+ Higher-order programming constructs and set operations are a natural fit for inspecting VCD signals, and this Python library allows you to easily specify, in text, what simulation timesteps matter to functional correctness.
40
+
41
+ ## Motivating Example
42
+ Say you are debugging a streaming interface, you often only care about the values of the data at timesteps meeting the following condition:
43
+
44
+ $\text{Rising edge} \land \text{Reset is 0} \land \text{ready} \land \text{valid}$
45
+
46
+ ![img/gtkwave.png](img/gtkwave.png "GTKWave screenshot of streaming interface we want to debug.")
47
+
48
+ You can filter through an individual signal with a filter function of this signature:
49
+
50
+ $(\text{Bits}, \text{Bits}, \text{Bits}) \rightarrow \text{Bool}$
51
+
52
+ with the left-hand tuple representing *values* at timestep $t$: $(t-1, t, t+1)$.
53
+
54
+ We then define our `get` method, which takes the name of the signal (as a String), a function with the above signature, and returns a set of *timesteps*:
55
+
56
+ $\texttt{get}: (\text{String}, ((\text{Bits}, \text{Bits}, \text{Bits}) \rightarrow \text{Bool})) \rightarrow \text{Set(Timestep)}$
57
+
58
+ As what is returned is a set, you can then use [set operations](https://en.wikipedia.org/wiki/Set_(mathematics)#Basic_operations) to manipulate them as needed, and finally extract the values from your desired signal using our `get_value` function:
59
+
60
+ $\texttt{get-value}: (\text{String}, \text{Set(Timestep)}) \rightarrow \text{List((Timestep, Bits))}$
61
+
62
+ Here's an example of finding the rising edges of the clock signal `TOP.clk` of our test wavefile `wave.vcd`:
63
+ ```python
64
+ from setVCD import SetVCD
65
+
66
+ # Load VCD file
67
+ vcd_path = "./tests/fixtures/wave.vcd"
68
+ sv = SetVCD(vcd_path, clock="TOP.clk")
69
+
70
+ rising_edges = sv.get("TOP.clk", lambda tm1, t, tp1: tm1 == 0 and t == 1)
71
+ print(rising_edges)
72
+ # {34, 36, 38, 40, 42, 44, ...}
73
+ ```
74
+
75
+ Because `rising_edges` is returned as a set, we can use set operations to combine it with other signals:
76
+ ```python
77
+ # Get times when the reset signal is 0
78
+ reset_is_0 = sv.get("TOP.reset", lambda tm1, t, tp1: t == 0)
79
+ # Use set intersection to get valid clock updates.
80
+ clock_update = rising_edges & reset_is_0
81
+ ```
82
+
83
+ Finally, you can search the wavefile with a regex (e.g. "output"), and apply the same operations to it:
84
+ ```python
85
+ # Find VCD signals relating to keyword "output"
86
+ sv.search("output")
87
+
88
+ # Get times when output_valid and output_ready are asserted.
89
+ out_valid = sv.get("TOP.Accelerator.io_output_valid", lambda tm1, t, tp1: t == 1)
90
+ out_ready = sv.get("TOP.Accelerator.io_output_ready", lambda tm1, t, tp1: t == 1)
91
+
92
+ # Get timesteps of valid outputs
93
+ valid_output_timesteps = rising_edges & reset0 & out_ready & out_valid
94
+
95
+ # Get the values of the Stream `value` signal (the data) at timesteps when it is valid
96
+ outputs = sv.get_values("TOP.Accelerator.io_output_payload_fragment_value_0[0:0]", valid_output_timesteps)
97
+ print(outputs)
98
+ # [(52, 0), (62, 0), (72, 1), ...] # Integer values
99
+ ```
100
+
101
+ ## Overview
102
+
103
+ SetVCD is a Python package for analyzing Verilog VCD files and extracting time points where specific signal conditions are met. It provides a simple, type-safe interface for working with simulation waveforms using set-based operations.
104
+
105
+ ## Installation
106
+ The package is available in PyPI:
107
+ ```bash
108
+ pip install setVCD
109
+ ```
110
+
111
+
112
+ ## Usage
113
+ ### Initialization
114
+
115
+ You can initialize SetVCD with either a filename or a vcdvcd object:
116
+
117
+ ```python
118
+ import setVCD
119
+ from pathlib import Path
120
+
121
+ # From string filename
122
+ sv = SetVCD("simulation.vcd", clock="clk")
123
+
124
+ # From Path object
125
+ sv = SetVCD(Path("simulation.vcd"), clock="clk")
126
+
127
+ # From vcdvcd object
128
+ import vcdvcd
129
+ vcd = vcdvcd.VCDVCD("simulation.vcd")
130
+ sv = SetVCD(vcd, clock="clk")
131
+ ```
132
+
133
+ The `clock` parameter must be the exact name of the clock signal in your VCD file (case-sensitive). This signal determines the time range for queries.
134
+
135
+ ### Signal Conditions
136
+
137
+ The `signal_condition` callback receives three arguments representing the signal value at three consecutive time points:
138
+
139
+ - `sm1`: Signal value at time-1 (None at time 0 or if value is x/z)
140
+ - `s`: Signal value at current time (None if value is x/z)
141
+ - `sp1`: Signal value at time+1 (None at last time or if value is x/z)
142
+
143
+ Signal values are `Optional[int]`:
144
+ - Integers: Binary values converted to decimal (e.g., "1010" → 10)
145
+ - None: Represents x/z values or boundary conditions (t-1 at time 0, t+1 at last time)
146
+
147
+ The callback should return `True` to include that time point in the result set.
148
+
149
+ ### Examples
150
+
151
+ #### Basic Signal Detection
152
+
153
+ ```python
154
+ # Rising edge: 0 -> 1 transition
155
+ rising = sv.get("clk", lambda sm1, s, sp1: sm1 == 0 and s == 1)
156
+
157
+ # Falling edge: 1 -> 0 transition
158
+ falling = sv.get("clk", lambda sm1, s, sp1: sm1 == 1 and s == 0)
159
+
160
+ # Any edge: value changed
161
+ edges = sv.get("data", lambda sm1, s, sp1: sm1 is not None and sm1 != s)
162
+
163
+ # Level high
164
+ high = sv.get("enable", lambda sm1, s, sp1: s == 1)
165
+
166
+ # Level low
167
+ low = sv.get("reset", lambda sm1, s, sp1: s == 0)
168
+ ```
169
+
170
+ #### Multi-bit Signals
171
+
172
+ ```python
173
+ # Specific pattern on a bus (binary "1010" = decimal 10)
174
+ pattern = sv.get("bus[3:0]", lambda sm1, s, sp1: s == 10)
175
+
176
+ # Bus is non-zero
177
+ active = sv.get("data[7:0]", lambda sm1, s, sp1: s != 0)
178
+
179
+ # Bus transition detection
180
+ bus_changed = sv.get("addr[15:0]", lambda sm1, s, sp1: sm1 is not None and sm1 != s)
181
+ ```
182
+
183
+ #### Complex Queries with Set Operations
184
+
185
+ ```python
186
+ # Rising clock edges when enable is high
187
+ clk_rising = sv.get("clk", lambda sm1, s, sp1: sm1 == 0 and s == 1)
188
+ enable_high = sv.get("enable", lambda sm1, s, sp1: s == 1)
189
+ valid_clocks = clk_rising & enable_high
190
+
191
+ # Data changes while not in reset
192
+ data_changes = sv.get("data", lambda sm1, s, sp1: sm1 is not None and sm1 != s)
193
+ not_reset = sv.get("reset", lambda sm1, s, sp1: s == 0)
194
+ valid_changes = data_changes & not_reset
195
+
196
+ # Either signal is high
197
+ sig1_high = sv.get("sig1", lambda sm1, s, sp1: s == 1)
198
+ sig2_high = sv.get("sig2", lambda sm1, s, sp1: s == 1)
199
+ either_high = sig1_high | sig2_high
200
+
201
+ # Exclusive high (one but not both)
202
+ exclusive_high = sig1_high ^ sig2_high
203
+ ```
204
+
205
+ #### Advanced Pattern Detection
206
+
207
+ ```python
208
+ # Detect setup violation: data changes right before clock edge
209
+ data_change = sv.get("data", lambda sm1, s, sp1: sm1 is not None and sm1 != s)
210
+ clk_about_to_rise = sv.get("clk", lambda sm1, s, sp1: s == 0 and sp1 == 1)
211
+ setup_violations = data_change & clk_about_to_rise
212
+
213
+ # Handshake protocol: valid and ready both high
214
+ valid_high = sv.get("valid", lambda sm1, s, sp1: s == 1)
215
+ ready_high = sv.get("ready", lambda sm1, s, sp1: s == 1)
216
+ handshake_times = valid_high & ready_high
217
+
218
+ # State machine transitions (binary "00" = 0, "01" = 1)
219
+ state_a = sv.get("state[1:0]", lambda sm1, s, sp1: s == 0)
220
+ state_b = sv.get("state[1:0]", lambda sm1, s, sp1: s == 1)
221
+ # Times when transitioning from state A to state B
222
+ transition = sv.get("state[1:0]", lambda sm1, s, sp1: sm1 == 0 and s == 1)
223
+ ```
224
+
225
+ ## Value Types
226
+
227
+ SetVCD supports three ValueType options to control how signal values are converted before being passed to your condition lambdas:
228
+
229
+ ### Raw() - Integer Conversion (Default)
230
+
231
+ Converts binary strings to decimal integers. X/Z values become `None`. This is the default behavior.
232
+
233
+ ```python
234
+ from setVCD import SetVCD, Raw
235
+
236
+ sv = SetVCD("simulation.vcd", clock="clk")
237
+
238
+ # Default behavior (Raw is implicit)
239
+ rising = sv.get("data[7:0]", lambda sm1, s, sp1: sm1 is not None and sm1 < s)
240
+
241
+ # Explicit Raw (same as above)
242
+ rising = sv.get("data[7:0]", lambda sm1, s, sp1: sm1 is not None and sm1 < s, value_type=Raw())
243
+
244
+ # Multi-bit signals converted to decimal
245
+ # Binary "00001010" → integer 10
246
+ high_values = sv.get("bus[7:0]", lambda sm1, s, sp1: s is not None and s > 128)
247
+ ```
248
+
249
+ ### String() - Preserve Raw Strings
250
+
251
+ Keeps vcdvcd's raw string representation, including X/Z values as literal strings. Useful for detecting unknown states.
252
+
253
+ ```python
254
+ from setVCD import SetVCD, String
255
+
256
+ sv = SetVCD("simulation.vcd", clock="clk")
257
+
258
+ # Detect X/Z values in data bus
259
+ has_x = sv.get("data[7:0]",
260
+ lambda sm1, s, sp1: s is not None and 'x' in s.lower(),
261
+ value_type=String())
262
+
263
+ # String pattern matching
264
+ all_ones = sv.get("bus[3:0]",
265
+ lambda sm1, s, sp1: s == "1111",
266
+ value_type=String())
267
+
268
+ # Get string values
269
+ values = sv.get_values("data", timesteps, value_type=String())
270
+ # Returns: [(50, "1010"), (60, "1111"), (70, "xxxx"), ...]
271
+ ```
272
+
273
+ **X/Z Handling:** X and Z values are preserved as strings (`"x"`, `"z"`, `"xxxx"`, etc.)
274
+
275
+ ### FP() - Fixed-Point to Float
276
+
277
+ Converts binary strings to floating-point by interpreting them as fixed-point numbers with configurable fractional bits and optional sign bit.
278
+
279
+ ```python
280
+ from setVCD import SetVCD, FP
281
+
282
+ sv = SetVCD("simulation.vcd", clock="clk")
283
+
284
+ # Temperature sensor with 8 fractional bits (Q8.8 format)
285
+ # Binary "0001100100000000" → 25.0 degrees
286
+ above_threshold = sv.get("temp_sensor[15:0]",
287
+ lambda sm1, s, sp1: s is not None and s > 25.5,
288
+ value_type=FP(frac=8, signed=False))
289
+
290
+ # Signed fixed-point (Q3.4 format - 1 sign bit, 3 integer bits, 4 fractional bits)
291
+ # Binary "11111110" → -0.125 (two's complement)
292
+ negative_values = sv.get("signed_value[7:0]",
293
+ lambda sm1, s, sp1: s is not None and s < 0,
294
+ value_type=FP(frac=4, signed=True))
295
+
296
+ # Get fixed-point values
297
+ voltages = sv.get_values("adc_reading[11:0]", timesteps,
298
+ value_type=FP(frac=12, signed=False))
299
+ # Returns: [(50, 1.2), (60, 2.5), (70, 3.8), ...]
300
+ ```
301
+
302
+ **X/Z Handling:** X and Z values become `float('nan')`. Use `math.isnan()` to detect them:
303
+
304
+ ```python
305
+ import math
306
+
307
+ # Filter out NaN values
308
+ valid_readings = sv.get("sensor",
309
+ lambda sm1, s, sp1: s is not None and not math.isnan(s),
310
+ value_type=FP(frac=8, signed=False))
311
+ ```
312
+
313
+ **Fixed-Point Formula:**
314
+ - Unsigned: `value = int_value / (2^frac)`
315
+ - Signed: Two's complement, then divide by `2^frac`
316
+
317
+ **Examples:**
318
+ - `"00001010"` with `frac=4, signed=False` → `10 / 16 = 0.625`
319
+ - `"11111110"` with `frac=4, signed=True` → `-2 / 16 = -0.125`
320
+ - `"00010000"` with `frac=0, signed=False` → `16 / 1 = 16.0` (integer)
321
+
322
+ ### Hardware Use Cases
323
+
324
+ **Raw (Default):** Most general-purpose verification - state machines, counters, addresses, data comparisons
325
+
326
+ **String:** Debugging X/Z propagation, detecting uninitialized signals, bit-pattern analysis
327
+
328
+ **FP:** Analog interfaces (ADC/DAC), sensor data, fixed-point DSP verification, power/temperature monitors
329
+
330
+ ## Future Enhancements
331
+
332
+ Planned for future versions:
333
+
334
+ - Higher-order operations for signal conditions
335
+ - Performance optimization for large VCD files
336
+ - Streaming interface for very large files
337
+ - MCP (Model Context Protocol) integration
@@ -0,0 +1,10 @@
1
+ setVCD/__init__.py,sha256=9XD5kj1dA6Ow_oI0TiEO3T9CKC4j3TuXRs25ZpOLA7E,923
2
+ setVCD/exceptions.py,sha256=0hXqETlYsmZ4sx8X2wJPIYOXA7OI82Nozq6wcgpae2Y,2149
3
+ setVCD/py.typed,sha256=uPBAYYlQuNeFaWWlnXX8ALU2iHi41MrB_u1ZSa3U4lA,81
4
+ setVCD/setVCD.py,sha256=yIRaYauPjXln5n6GuOHBcUgPbr-xbcj37CJfNLJQBpI,17139
5
+ setVCD/types.py,sha256=OZLJszYveOr6pxU36O7qhub_GrJLcefkfZUXiwsL3IM,5644
6
+ setvcd-0.2.0.dist-info/licenses/LICENSE,sha256=rHs-K10ejk3J6IkzDG_iJseZllEVzrR48gEwyqehK_g,1077
7
+ setvcd-0.2.0.dist-info/METADATA,sha256=8WOqJrH2QIhh1puZWnMooa4iGy0v0tiQMsw_b47cdqw,12142
8
+ setvcd-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ setvcd-0.2.0.dist-info/top_level.txt,sha256=CRByAnG-0SIIj8FXsocVOHmwoInY5FQFuRHnyCqS6mo,7
10
+ setvcd-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 VCD2Set Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ setVCD