Waveform analysis¶
Operations on a Waveform return new Waveform objects, keeping the
.cycle and .time axes aligned when filtering or transforming samples, so
results can always be traced back to the original simulation.
Arithmetic and bitwise operations¶
Waveforms support element-wise arithmetic, comparison, bitwise, and shift operations with other waveforms or numeric scalars:
from wavekit import VcdReader
with VcdReader('simulation.vcd') as reader:
with reader.clock_domain(clock='tb.clk'):
write_pointer = reader['tb.fifo.w_ptr'].w
read_pointer = reader['tb.fifo.r_ptr'].w
depth = 8
occupancy = (write_pointer + depth - read_pointer) % depth
Loaded waveforms are interpreted as unsigned by default. Pass signed=True to
Signal.waveform() or load_waveform() when a signal should be interpreted
as signed. Two waveform operands must have the same signedness. Convert an
existing waveform with .as_signed() or .as_unsigned() when necessary.
Filter and slice¶
Use mask() with a boolean NumPy array or a one-bit waveform. Use filter()
when the condition is easiest to express as a scalar callback:
known = data.filter(lambda value: value != 0)
active = data.mask(valid == 1)
first_cycles = data.cycle_slice(0, 100)
window = data.time_slice(start_time=10_000, end_time=20_000)
By default, cycle_slice(start, end) and time_slice(start, end) use a
half-open range: the start is included and the end is excluded. Their bounds are
absolute cycle numbers or simulation timestamps, not array indices. Use
slice() or take() when you need array-index operations.
Relative access¶
Use relative(), ahead(), and back() when a calculation needs neighboring
samples while retaining the original axes:
The default boundary behavior repeats the first or last value. Use the pad and
pad_value arguments when a different boundary policy is required.
Bit operations and composition¶
Bit selection follows Verilog-style indexing:
Use countones() and countbits() to count set or cleared bits in each
sample. Use onehot() and onehot0() to test whether exactly one, or at most
one, bit is set:
set_bits = data.countones()
zero_bits = data.countbits(0)
one_selected = grants.onehot()
zero_or_one_selected = grants.onehot0()
split_bits() accepts either an integer for equal-width groups or a list of
explicit widths. Groups are returned from least significant to most significant:
byte_fields = data.split_bits(8)
# byte_fields[0] contains bits [7:0], byte_fields[1] contains bits [15:8], and so on.
low, middle, high = data.split_bits([4, 8, 20])
# low contains bits [3:0], middle bits [11:4], and high bits [31:12].
Use Waveform.concatenate() to combine same-length fields into a wider
waveform. The first field occupies the least-significant bits:
Use Waveform.merge() to combine corresponding samples with a custom function:
# Compute a majority vote across three 1-bit signals at each sample.
majority = Waveform.merge(
[a, b, c],
lambda values: int(sum(values) >= 2),
width=1,
signed=False,
)
Transitions and reduction¶
Use changed() and stable() to compare each sample with the previous one.
They are complementary: the first is true when values differ, while the second
is true when they match. At the first sample, changed() is false and
stable() is true:
occupancy_changes = occupancy.changed()
occupancy_stable = occupancy.stable()
change_cycles = occupancy_changes.cycle[occupancy_changes.value]
For one-bit signals, use rising_edge() and falling_edge() to locate the
start and end of an active interval:
starts = valid.rising_edge()
stops = valid.falling_edge()
start_times = starts.time[starts.value]
stop_times = stops.time[stops.value]
Use unique_consecutive() to keep the first sample of each consecutive run, or
compress() to also preserve the final sample of the waveform. Use
downsample() to aggregate consecutive chunks:
one_per_run = data.unique_consecutive()
compact = data.compress()
summary = data.downsample(100) # average each 100-sample chunk
Extract NumPy results¶
Use .value when handing data to NumPy or another analysis library:
import numpy as np
print(f'Average active value: {np.mean(active.value):.2f}')
print(f'Maximum active value: {np.max(active.value)}')
Keep the Waveform itself until you no longer need cycle or timestamp context.