.sigproc - Signal Processing Utilities

General signal processing utilities for PyBERT.

Original author: David Banas <capn.freako@gmail.com>

Original date: June 16, 2024

Copyright (c) 2024 David Banas; all rights reserved World wide.

A partial extraction of the old pybert/utility.py, as part of a refactoring.

pybert.utility.sigproc.add_ffe_dfe(ffe_weights: numpy.typing.NDArray.~Real, dfe_weights: numpy.typing.NDArray.~Real, nspui: int, pr_ctle_out: numpy.typing.NDArray.~Real) numpy.typing.NDArray.~Real[source]

Add the effects of the FFE and DFE to the cumulative system pulse response at the CTLE output.

Parameters:
  • ffe_weights – List of FFE tap weights.

  • dfe_weights – List of DFE tap weights.

  • nspui – Number of vector samples per unit interval.

  • pr_ctle_out – Cumulative system pulse response at CTLE output.

Returns:

Complete system pulse response, including the effects of the Rx FFE and DFE.

Raises:

ValueError – If given post-CTLE pulse response is clearly non-optimum. (Used to improve overall performance of exhaustive optimization.)

pybert.utility.sigproc.calc_eye(ui: float, samps_per_ui: int, height: int, ys: numpy.typing.NDArray.~Real, y_max: numpy.floating.typing.Any, clock_times: numpy.typing.NDArray.~Real | None = None) numpy.typing.NDArray[source]

Calculates the “eye” diagram of the input signal vector.

Parameters:
  • ui – unit interval (s)

  • samps_per_ui – # of samples per unit interval

  • height – height of output image data array

  • ys – signal vector of interest

  • y_max – max. +/- vertical extremity of plot

Keyword Arguments:

clock_times – Vector of clock times to use for eye centers. If not provided, just use mean zero-crossing and assume constant UI and no phase jumps. (This allows the same function to be used for eye diagram creation, for both pre and post-CDR signals.) Default: None

Returns:

The “heat map” representing the eye diagram. Each grid location contains a value indicating the number of times the signal passed through that location.

pybert.utility.sigproc.calc_resps(t: numpy.typing.NDArray.~Real, h: numpy.typing.NDArray.~Real, ui: float, f: numpy.typing.NDArray.~Real, eps: float = 1e-18) tuple[numpy.typing.NDArray.~Real, numpy.typing.NDArray.~Real, numpy.typing.NDArray.~Comp][source]

From a uniformly sampled impulse response, calculate the: step, pulse, and frequency responses.

Parameters:
  • t – Time vector associated with h (s).

  • h – Impulse response (V/sample).

  • ui – Unit interval (s).

  • f – Frequency vector associated w/ H (Hz).

Keyword Arguments:

eps – Threshold for floating point equality. Default: 1e-18

Returns:

Tuple containing

  • step,

  • pulse, and

  • frequency responses.

Raises:
  • ValueError – If t is not uniformly spaced.

  • ValueError – If length of t is not at least length of h.

  • ValueError – If f is not uniformly spaced.

  • ValueError – If f does not begin with zero.

Notes

1. t is assumed to be uniformly spaced and monotonic. (It is not assumed to begin at zero.)

pybert.utility.sigproc.get_dfe_weights(dfe_taps: list[~pybert.models.tx_tap.TxTapTuner], pr: numpy.typing.NDArray.~Real, nspui: int) numpy.typing.NDArray.~Real[source]

Get the ideal DFE tap weights, given the tap tuners and pulse response, using PRZF.

Parameters:
  • dfe_taps – List of TxTapTuners governing DFE tap weight ranges.

  • pr – Pulse response to be equalized.

  • nspui – Number of vector samples per unit interval.

Returns:

List of ideal DFE tap weights.

pybert.utility.sigproc.import_time(filename: str, sample_per: float) numpy.typing.NDArray.~Real[source]

Read in a time domain waveform file, resampling as appropriate, via linear interpolation.

Parameters:
  • filename – Name of waveform file to read in.

  • sample_per – New sample interval

Returns:

Resampled waveform.

pybert.utility.sigproc.interp_time(ts: numpy.typing.NDArray.~Real, xs: numpy.typing.NDArray.~Real, sample_per: float) numpy.typing.NDArray.~Real[source]

Resample time domain data, using linear interpolation.

Parameters:
  • ts – Original time values.

  • xs – Original signal values.

  • sample_per – System sample period (ts).

Returns:

Resampled waveform.

pybert.utility.sigproc.make_ctle(rx_bw: float, peak_freq: float, peak_mag: float, w: numpy.typing.NDArray.~Real) tuple[numpy.typing.NDArray.~Real, numpy.typing.NDArray.~Comp][source]

Generate the frequency response of a continuous time linear equalizer (CTLE), given the:

  • signal path bandwidth,

  • peaking specification, and

  • list of frequencies of interest.

Parameters:
  • rx_bw – The natural (or, unequalized) signal path bandwidth (Hz).

  • peak_freq – The location of the desired peak in the frequency response (Hz).

  • peak_mag – The desired relative magnitude of the peak (dB).

  • w – The list of frequencies of interest (rads./s).

Returns:

Tupple containing

  • w, the frequencies at which H has been sampled (rad./s), and

  • H, the resultant complex frequency response, at the given frequencies.

Notes

1. We use the ‘invres()’ function from scipy.signal, as it suggests itself as a natural approach, given our chosen use model of having the user provide the peaking frequency and degree of peaking. That is, we define our desired frequency response using one zero and two poles, where

  • The pole locations are equal to

    • the signal path natural bandwidth, and

    • the user specified peaking frequency.

  • The zero location is chosen, so as to provide the desired degree of peaking.

pybert.utility.sigproc.make_uniform(t: numpy.typing.NDArray.~Real, jitter: numpy.typing.NDArray.~Real, ui: float, nbits: int) tuple[numpy.typing.NDArray.~Real, list[int]][source]

Make the jitter vector uniformly sampled in time, by zero-filling where necessary.

The trick, here, is creating a uniformly sampled input vector for the FFT operation, since the jitter samples are almost certainly not uniformly sampled. We do this by simply zero padding the missing samples.

Parameters:
  • t – The sample times for the ‘jitter’ vector.

  • jitter – The input jitter samples.

  • ui – The nominal unit interval.

  • nbits – The desired number of unit intervals, in the time domain.

Returns:

A pair consisting of

  • The uniformly sampled, zero padded jitter vector.

  • The indices where y is valid (i.e. - not zero padded).

pybert.utility.sigproc.moving_average(a: numpy.typing.NDArray.~Real, n: int = 3) numpy.typing.NDArray.~Real[source]

Calculates a sliding average over the input vector.

Uses a weighted averaging kernel, to preserve singularity of peak location in input data.

Parameters:

a – Input vector to be averaged.

Keyword Arguments:

n – Width of averaging window, in vector samples. Odd numbers work best. Default: 3

Returns:

The moving average of the input vector, leaving the input vector unchanged.

Notes

  1. The odd code is intended to “protect” the first/last elements of the input vector from the averaging process. In PyBERT those elements “collect” the missed edges when assembling the TIE. Because of this non-standard use, those bins shouldn’t be included in averaging.

pybert.utility.sigproc.pulse_center(p: numpy.typing.NDArray.~Real, nspui: int) tuple[int, float][source]

Determines the center of the pulse response, using the “Hula Hoop” algorithm (See SiSoft/Tellian’s DesignCon 2016 paper.)

Parameters:
  • p – The single bit pulse response.

  • nspui – The number of vector elements per unit interval.

Returns:

A pair containing

  • the estimated index at which the clock will sample the main lobe, and

  • the vertical threshold at which the main lobe is one UI wide.

pybert.utility.sigproc.raised_cosine(x: numpy.typing.NDArray.~Comp) numpy.typing.NDArray.~Comp[source]

Apply raised cosine window to input.

pybert.utility.sigproc.resize_zero_pad(x: numpy.typing.NDArray.~Real, new_length: int, pad_front: bool = False) numpy.typing.NDArray.~Real[source]

Resize an array, zero padding if necessary.

Parameters:
  • x – Array to resize.

  • new_length – Desired new length of array.

Keyword Arguments:

pad_front – Apply any necessary padding to front of input vector when True. Default: False

Returns:

Resized array, zero padded if longer than original.

Notes

1. This function is necessary to cover a funny corner case w/ NumPy. The numpy.resize() function repeats the input array when asked to extend its length, which is not what we want in PyBERT. And the numpy.ndarray.resize() function, which zero pads instead, is fragile, due to data ownership.

pybert.utility.sigproc.trim_impulse(g: numpy.typing.NDArray.~Real, min_len: int = 0, max_len: int = 1000000, front_porch: int = 0, kept_energy: float = 0.999) tuple[numpy.typing.NDArray.~Real, int][source]

Trim impulse response, for more useful display, by:

  • clipping off the tail, after given portion of the total first derivative power has been captured, and

  • enforcing a minimum “front porch” length if requested.

Parameters:

g – Response to trim.

Keyword Arguments:
  • min_len – Minimum length of returned vector. Default: 0

  • max_len – Maximum length of returned vector. Default: 1000000

  • front_porch – Minimum allowed “front porch” length. Default: 0

  • kept_energy – The portion of first derivative “energy” retained. Default: 99.9%

Returns:

A pair consisting of

  • the trimmed response, and

  • the index of the chosen starting position.