<jupyter_start><jupyter_text>Using PyBERT as a LibraryThis Jupyter notebook provides an example of using the functionality of the _PyBERT_ Python package, not as a stand alone GUI application, but rather as a library, importing into a higher level Python script.Original author: <NAME>   Original date:   June 28, 2021Copyright &copy; 2021 <NAME>, Inc.; all rights reserved World wide. Table of Contents1&nbsp;&nbsp;Introduction2&nbsp;&nbsp;Global Defines3&nbsp;&nbsp;Imports4&nbsp;&nbsp;PyBERT IBIS-AMI Link Simulation4.1&nbsp;&nbsp;IBIS-AMI Link Diagram and Nomenclature4.2&nbsp;&nbsp;PyBERT Object4.3&nbsp;&nbsp;Notes4.4&nbsp;&nbsp;Interconnect Frequency Response &amp; Characteristic Impedance4.5&nbsp;&nbsp;Interconnect S-parameters4.6&nbsp;&nbsp;Interconnect Z-parameters4.7&nbsp;&nbsp;Complete Channel Frequency Response5&nbsp;&nbsp;Channel Step Response6&nbsp;&nbsp;Channel Eye Diagram6.1&nbsp;&nbsp;Notes7&nbsp;&nbsp;Tx Deemphasis7.1&nbsp;&nbsp;Notes8&nbsp;&nbsp;Rx CTLE8.1&nbsp;&nbsp;Notes9&nbsp;&nbsp;Combined Tx/Rx Equalization9.1&nbsp;&nbsp;Notes10&nbsp;&nbsp;DFE10.1&nbsp;&nbsp;Notes11&nbsp;&nbsp;Jitter Decomposition &amp; Bathtub Curve Plotting12&nbsp;&nbsp;Final Touches12.1&nbsp;&nbsp;Random Noise12.2&nbsp;&nbsp;CDR Induced Jitter  IntroductionIn the typical use case, [PyBERT](https://github.com/capn-freako/PyBERT/wiki) is run as a stand alone GUI application.However, it is also possible to draw from the _PyBERT_ package, as a _library_, importing only certain needed features into a larger _Python_ script.This [Jupyter](https://jupyter.org/) notebook provides an example of doing just that.Furthermore, this example shows how to import _PyBERT_ functionality into a higher level _Python_ script, when [IBIS-AMI models](https://www.eetimes.com/ami-models-what-why-and-how/) are being used, in place of PyBERT's native Tx/Rx models.We'll take as our design goal PyBERT's default: attempting to move a 10 Gbps symbol stream through 1 meter of 28 gauge unshielded twisted pair (UTP) cabling.  Global DefinesIf you get lucky, you might get away w/ only editing this section.<jupyter_code>TX_IBIS_FILE = "../../ibisami/example/example_tx.ibs"
RX_IBIS_FILE = "../../ibisami/example/example_rx.ibs"<jupyter_output><empty_output><jupyter_text>ImportsHere, we import everything we'll need.(I'm not sure why, but these notebooks tend to work more smoothly if you import everything first, before executing any other Python code.)<jupyter_code>%matplotlib inline

import sys
import pprint

print(f"Python executable: {sys.executable}")
print(f"Python exec_prefix: {sys.exec_prefix}")
print(f"Python prefix: {sys.prefix}")
print("Python import search path:")
pprint.pp(sys.path)

import matplotlib.pyplot as plt
import numpy             as np
import scipy             as sp
import scipy.signal      as sig
import skrf              as rf

import pybert            as pb
import pyibisami         as pyami

from pybert.pybert       import PyBERT
from pybert.pybert_cntrl import my_run_simulation
from pybert.pybert_util  import calc_eye, find_crossings, calc_jitter

from pyibisami.ami_model import AMIModel, AMIModelInitializer

print(f"PyBERT version: {pb.__version__}")
print(f"\tat: {pb.__file__}")

print(f"PyAMI version: {pyami.__version__}")
print(f"\tat: {pyami.__file__}")

print(f"NumPy version: {np.__version__}")
print(f"\tat: {np.__file__}")

print(f"SciPy version: {sp.__version__}")
print(f"\tat: {sp.__file__}")

print(f"SciKit-RF version: {rf.__version__}")
print(f"\tat: {rf.__file__}")

plt.rcParams['figure.figsize'] = [12, 8]<jupyter_output>Python executable: /Users/dbanas/opt/anaconda3/envs/pybert/bin/python
Python exec_prefix: /Users/dbanas/opt/anaconda3/envs/pybert
Python prefix: /Users/dbanas/opt/anaconda3/envs/pybert
Python import search path:
['/Users/dbanas/Documents/Projects/PyBERT/misc',
 '/Users/dbanas/opt/anaconda3/envs/pybert/lib/python38.zip',
 '/Users/dbanas/opt/anaconda3/envs/pybert/lib/python3.8',
 '/Users/dbanas/opt/anaconda3/envs/pybert/lib/python3.8/lib-dynload',
 '',
 '/Users/dbanas/opt/anaconda3/envs/pybert/lib/python3.8/site-packages',
 '/Users/dbanas/opt/anaconda3/envs/pybert/lib/python3.8/site-packages/IPython/extensions',
 '/Users/dbanas/.ipython']
PyBERT version: 3.5.2
	at: /Users/dbanas/opt/anaconda3/envs/pybert/lib/python3.8/site-packages/pybert/__init__.py
PyAMI version: 3.5.1
	at: /Users/dbanas/opt/anaconda3/envs/pybert/lib/python3.8/site-packages/pyibisami/__init__.py
NumPy version: 1.21.2
	at: /Users/dbanas/opt/anaconda3/envs/pybert/lib/python3.8/site-packages/numpy/__init__.py
SciPy versio[...]<jupyter_text>PyBERT IBIS-AMI Link SimulationIn this section we conduct our first IBIS-AMI link simulation using PyBERT.  IBIS-AMI Link Diagram and NomenclatureThe IBIS-AMI simulation community uses some very specific nomenclature, to describe different parts of a serial communication link.And correct usage of this nomenclature is necessary, in order to avoid confusion.The diagram below illustrates how a serial communication link is conceptualized by an IBIS-AMI modeler and provides the correct nomenclature.![IBIS-AMI Link Diagram & Nomenclature](IBIS-AMI_Link_Diagram.png)  PyBERT ObjectHere, we instantiate and configure our _PyBERT_ object and run a trial simulation of its default channel.**Note:** You may need to dismiss some GUI dialog alerts.<jupyter_code># Instantiate a PyBERT object and change any defaults as required.
myPyBERT = PyBERT(run_simulation=False,  # Need to adjust some properties first.
                  gui=False)             # Don't run stand-alone; we're in a notebook.
# myPyBERT.debug = True  # Uncomment to debug.

# Instantiate/verify the IBIS-AMI files.
myPyBERT.tx_ibis_file = TX_IBIS_FILE  # Changing the `tx_ibis_file` parameter
assert (myPyBERT.tx_ibis_valid)       # triggers a reloading of the IBIS model.
assert (myPyBERT.tx_dll_file)         # We're expecting to be given IBIS-AMI models,
assert (myPyBERT.tx_ami_file)         # which must include *.AMI and *.DLL files.
assert (myPyBERT.tx_ami_valid)
assert (myPyBERT.tx_dll_valid)

myPyBERT.rx_ibis_file = RX_IBIS_FILE
assert (myPyBERT.rx_ibis_valid)
assert (myPyBERT.rx_dll_file)
assert (myPyBERT.rx_ami_file)
assert (myPyBERT.rx_ami_valid)
assert (myPyBERT.rx_dll_valid)

# Tell PyBERT to use the I/O IBIS models, instead of its native models.
myPyBERT.tx_use_ibis = True
myPyBERT.rx_use_ibis = True

# Run the simulation and print out the resultant log for sanity checking.
my_run_simulation(myPyBERT,
                  initial_run=True,    # PyBERT does some "housekeeping" the first time through, which shouldn't be repeated.
                  update_plots=False)  # We'll do our own plotting, here in this notebook.
print(myPyBERT.console_log)

# Grab some resultant data needed below.
ui         = myPyBERT.ui                         # unit interval
nspb       = myPyBERT.nspb                       # samples per bit
t          = myPyBERT.t                          # simulation time vector
Ts         = t[1]                                # simulation sample interval
eye_bits   = myPyBERT.eye_bits                   # number of UIs to use for eye plotting.
ch_s       = myPyBERT.chnl_s                     # channel step response
Zref       = myPyBERT.Rs                         # Reference (i.e. - port) impedance of generated networks.
f_10MHz_ix = np.where(myPyBERT.f >= 10e6)[0][0]  # 10 MHz index into the PyBERT frequency vector
# We use 10 MHz as a proxy for "d.c." below.<jupyter_output>PyBERT Console Log


[2022-01-03 07:44:55.573920]: PyBERT: Started.

[2022-01-03 07:44:55.573954]: PyBERT: System: Darwin 18.7.0

[2022-01-03 07:44:55.573965]: PyBERT: Python Version: 3.8.12

[2022-01-03 07:44:55.573970]: PyBERT: PyBERT Version: 3.5.2

[2022-01-03 07:44:55.573975]: PyBERT: PyAMI Version: 3.5.1

[2022-01-03 07:44:55.573982]: PyBERT: GUI Toolkit: qt

[2022-01-03 07:44:55.573988]: PyBERT: Kiva Backend: qpainter

[2022-01-03 07:44:57.348403]: PyBERT: Parsing Tx IBIS file, '../../ibisami/example/example_tx.ibs'...

[2022-01-03 07:44:57.391634]: PyBERT: Result:
Success!

[2022-01-03 07:44:57.397286]: PyBERT: Parsing Tx AMI file, '../../ibisami/example/example_tx.ami'...

[2022-01-03 07:44:57.402964]: PyBERT: Success.

[2022-01-03 07:44:57.403457]: PyBERT: Parsing Rx IBIS file, '../../ibisami/example/example_rx.ibs'...

[2022-01-03 07:44:57.444024]: PyBERT: Result:
Success!

[2022-01-03 07:44:57.465018]: PyBERT: Parsing Rx AMI file, '../../ibisami/example/example_rx.ami'...<jupyter_text>Notes1. Channel delay: {{f"{(myPyBERT.chnl_dly * 1e9):5.2f} ns"}}1. Tx driver output impedance: {{f"{myPyBERT.Rs:5.0f} Ohms"}}1. Tx driver parasitic capacitance: {{f"{myPyBERT.Cs*1e12:5.1f} pF"}}1. Rx input resistance: {{f"{myPyBERT.RL:5.0f} Ohms"}}1. Rx input capacitance: {{f"{myPyBERT.Cp*1e12:5.1f} pF"}}  Interconnect Frequency Response & Characteristic ImpedanceHere we plot the calculated raw _frequency response_ and _characteristic impedance_ for the interconnect, for sanity checking.**Notes:**1. The frequency response shown here would be equivalent to the $S_{21}$ of a 2-port network representing just the interconnect and with port impedances precisely matched to the interconnect characteristic impedance, at _every_ frequency.That is why no ripples are observed.1. Note that this interconnect has a raw bandwidth below 1 GHz.    **Question:** Would you expect it to support our target Baud rate (10 GBaud)?1. Our native PyBERT interconnect has a characteristic impedance slightly larger than the reference: $100 \Omega$.<jupyter_code>print(f"Zref = {Zref:3.0f} Ohms")
plt.semilogx(myPyBERT.f/1e9, 20*np.log10(abs(myPyBERT.H)[:len(myPyBERT.f)]),       label="H(f)")
plt.semilogx(myPyBERT.f/1e9, 20*np.log10(abs(myPyBERT.Zc)[:len(myPyBERT.f)]/Zref), label="Zc(f) (100 Ohms)")
plt.title("Interconnect Transfer Function and Characteristic Impedance")
plt.xlabel("Frequency (GHz)")
plt.ylabel("|H/Zc| (dB)")
plt.axis(ymax=3, ymin=-20)
plt.axhline(-3, color='k', linestyle='dashed', label='-3 dB')
plt.legend()
plt.grid()
plt.show()<jupyter_output>Zref = 100 Ohms<jupyter_text>Interconnect S-parametersHere we generate and plot the calculated 2-port network for the _interconnect_, for sanity checking.**Notes:**1. This does _not_ include the actual termination at the Rx input.  (Here, the line is assumed to be terminated into the reference impedance.)1. The reference impedance is set equal to the Tx output driver _differential_ impedanceand assumed real.1. Because the network is calculated, as opposed to measured, it is perfectly symmetrical.Therefore, S11 perfectly overlaps S22 and likewise for S21/S12.That is why only two traces seem to be present.<jupyter_code>print(f"Zref = {Zref:3.0f} Ohms")
myPyBERT.ch_s2p.plot_s_db(title="Unterminated Channel S-parameters", logx=True)
plt.axis(ymin=-60)
plt.axhline(-3, color='k', linestyle='dashed', label='-3 dB')
plt.legend()
plt.grid()
plt.show()<jupyter_output>Zref = 100 Ohms<jupyter_text>Interconnect Z-parametersHere we plot the calculated 2-port Z-parameters of the interconnect, for sanity checking.The plot characteristics can be explained as follows:1. The resonance peaks are due to multiple full positive reflections at both ends of the line.    - Z-parameters of a 2-port network are taken with a current source driving one port and the other port open circuited. And both of those terminations exhibit infinite impedance.    - The position of the first resonance peak is correct, as per:        $$        \begin{eqnarray}        f =& \frac{1}{\tau_{round trip}} \\        =& \frac{1}{10 ns} \\        =& 100 MHz        \end{eqnarray}        $$1. The decay in the amplitude of the resonance peaks is due to increasing loss through the interconnect at high frequency, which lessens the number of significant reflections contributing to the superposition.1. The trend toward infinite impedance at very low frequency is real and correct and due to the lack of any d.c. shunt path in the interconnect itself.  1. The asymptotic decay of Z11/Z22 to approximately 100 $\Omega$ is due the high frequency loss through the interconnect being so great that not even the first reflection makes it back to the driven port with any significant amplitude, and all that is "seen" by the entering wave is the characteristic impedance of the interconnect.1. The asymptotic decay to zero of Z21/Z12 is for the same reason: no appreciable energy is making it to the far end of the interconnect, at those high frequencies.<jupyter_code>print(f"Zref = {Zref:3.0f} Ohms")
myPyBERT.ch_s2p.plot_z_mag(title="Channel Z-parameters", logx=True)
plt.axis(ymin=0, ymax=2e3)
plt.show()<jupyter_output>Zref = 100 Ohms<jupyter_text>Complete Channel Frequency ResponseHere we plot the frequency response of the _complete_ channel, including the _real_ Tx and Rx terminations (including their parasitic capacitance), for sanity checking.**Notes:**1. The ripples are due to multiple reflections caused by the imperfect Tx and Rx terminations.1. The inclusion of the parasitic capacitances of the Tx and Rx terminations has _not_ appreciably shifted the bandwidth of this interconnect.<jupyter_code>print(f"Zref = {Zref:3.0f} Ohms")
plt.semilogx(myPyBERT.f/1e9, 20*np.log10(abs(myPyBERT.chnl_H)[:len(myPyBERT.f)]), label="chnl_H")
plt.title("Frequency Response of Complete Channel (real term.)")
plt.xlabel("Frequency (GHz)")
plt.ylabel("|H(f)| (dB)")
plt.axhline(-3, color='k', linestyle='dashed', label='-3 dB')
plt.axis(ymax=1, ymin=-20)
plt.grid()
plt.legend()
plt.show()<jupyter_output>Zref = 100 Ohms<jupyter_text>_Channel_ Step ResponseHere we plot the _channel_ step response corresponding to the simulation just run.**Notes:**1. The _Channel_ step response includes everything through and including the Rx analog termination, but no equalization.1. Don't be alarmed that the position of the edge in the step response doesn't agree with the reported channel delay, above. PyBERT "trims" the step response for convenience/efficiency.<jupyter_code>plt.plot (t[:len(ch_s)]*1e9, ch_s[:len(t)], label="Channel")
plt.title("Channel Step Responses")
plt.xlabel("Time (ns)")
plt.legend()
plt.grid()
plt.show()<jupyter_output><empty_output><jupyter_text>_Channel_ Eye DiagramLet's take a look at the "eye" diagram for the channel alone.That will give us some idea of whether or not we need to add any equalization.<jupyter_code>def plotsig(sig):
    """Plot last 100 unit intervals, as well as the 'eye' diagram, of a signal.
    """
    ys    = sig[-eye_bits*nspb:]
    y_max = np.max(ys) * 1.1
    img   = calc_eye(ui, nspb, 100, ys, y_max)
    plt.subplot(121)
    plt.plot(sig[-100*nspb:])
    plt.subplot(122)
    plt.hot()
    plt.imshow(img, aspect="auto")
    plt.show()

plotsig(myPyBERT.ctle_out)  # In the PyBERT simulation engine, `ctle_out` is the slicer input.<jupyter_output><empty_output><jupyter_text>Notes1. The eye is completely closed.(This shouldn't come as a surprise, given that the bandwidth of our interconnect is < 1 GHz and, yet, we're attempting to push a 10 GBaud symbol stream through it.)  Tx DeemphasisThe eye above is completely closed.Here we investigate the effect of adding Tx deemphasis on the system eye.**Note:** If you changed the Tx IBIS file name, above, then you'll probably get an error like this, the first time you run this cell:```ValueError: Failed parameter tree search looking for: ; available keys: dict_keys(['tx_tap_nm2', 'tx_tap_np1', 'tx_tap_units', 'tx_tap_nm1'])```Just swap in one of the listed available keys, in lines 2/3, and you should be good to go.<jupyter_code># myPyBERT._tx_cfg.configure_traits()  # Use the GUI configurator, for now.
myPyBERT._tx_cfg.set_param_val(["Model_Specific","tx_tap_np1"], 3)  # Valid range: 1-10.
myPyBERT._tx_cfg.set_param_val(["Model_Specific","tx_tap_nm1"], 5)  # Bypass the GUI config.
# print(myPyBERT._tx_cfg.fetch_param_val(["Model_Specific","tx_tap_nm1"]))
myPyBERT.tx_use_ami     = True
# myPyBERT.tx_use_getwave = True
my_run_simulation(myPyBERT, initial_run=False, update_plots=False)
print(myPyBERT.console_log)

ui   = myPyBERT.ui           # unit interval
nspb = myPyBERT.nspb         # samples per bit
t    = myPyBERT.t            # simulation time vector
Ts   = t[1]                  # simulation time interval
eye_bits = myPyBERT.eye_bits # number of UIs to use for eye plotting.
plotsig(myPyBERT.ctle_out)<jupyter_output><empty_output><jupyter_text>Notes1. We've been able to open our eye nicely, using Tx deemphasis.  Rx CTLEHere, we investigate the effectiveness of the Rx CTLE as an alternative to the Tx FFE.<jupyter_code># Turn off the Tx deemphasis.
myPyBERT.tx_use_ami = False

# Turn on the Rx CTLE.
myPyBERT.rx_use_ami = True
# myPyBERT.rx_use_getwave = True
myPyBERT._rx_cfg.set_param_val(["Model_Specific","ctle_mode"],      1)
myPyBERT._rx_cfg.set_param_val(["Model_Specific","ctle_bandwidth"], 12e9)
myPyBERT._rx_cfg.set_param_val(["Model_Specific","ctle_freq"],      5e9)
myPyBERT._rx_cfg.set_param_val(["Model_Specific","ctle_mag"],       6)

# Rerun the simulation and plot the results.
myPyBERT.console_log = ""
my_run_simulation(myPyBERT, initial_run=False, update_plots=False)
print(myPyBERT.console_log)

plotsig(myPyBERT.ctle_out)<jupyter_output><empty_output><jupyter_text>Notes1. We've also been able to open our eye, using the Rx CTLE, although this results is a distinctly different shape to our eye.  Combined Tx/Rx EqualizationHere, we investigate the effectiveness of using both the Tx Deemphasis and the Rx CTLE.<jupyter_code># Turn the Tx de-emphasis back on.
myPyBERT.tx_use_ami = True
myPyBERT.console_log = ""
my_run_simulation(myPyBERT, initial_run=False, update_plots=False)
print(myPyBERT.console_log)

plotsig(myPyBERT.ctle_out)<jupyter_output><empty_output><jupyter_text>Notes1. The combined usage of both Tx deemphasis and Rx CTLE has yielded a nicely open eye.  DFEGiven the quality of the eye above, there's no real need for a DFE.However, we'll try one here, for the sake of completeness.<jupyter_code>myPyBERT.use_dfe    = True
# Tx FFE post-cursor taps are often redundant w/ DFE inclusion.
myPyBERT._tx_cfg.set_param_val(["Model_Specific","tx_tap_nm1"], 0)
myPyBERT.console_log = ""
my_run_simulation(myPyBERT, initial_run=False, update_plots=False)
print(myPyBERT.console_log)

plotsig(myPyBERT.dfe_out)<jupyter_output><empty_output><jupyter_text>Notes1. Eye quality is roughly equivalent to the _Combined Tx/Rx Equalization_ case.And, since DFEs are often expensive and power hungry components, we elect to forgo a DFE for this link design.1. The `DFE: not present` line in the output above may be confusing.That line is coming from the IBIS-AMI model and we have that model's DFE deselected, in favor of using PyBERT's native DFE modeling, instead.  Jitter Decomposition & Bathtub Curve PlottingHere, we decompose the observed jitter into its different components and use that information to plot "bathtub" curves.For this, we'll return to the pre-DFE simulation configuration.<jupyter_code>myPyBERT.use_dfe    = False
myPyBERT._tx_cfg.set_param_val(["Model_Specific","tx_tap_nm1"], 5)
myPyBERT.console_log = ""
my_run_simulation(myPyBERT, initial_run=False, update_plots=False)
print(myPyBERT.console_log)

plotsig(myPyBERT.ctle_out)

ideal_xings  = find_crossings(t, myPyBERT.tx_out)
actual_xings = find_crossings(t, myPyBERT.ctle_out)
(   jitter,
    t_jitter,
    isi,
    dcd,
    pj,
    rj,
    jitter_ext,
    thresh,
    jitter_spectrum,
    jitter_ind_spectrum,
    spectrum_freqs,
    hist,
    hist_synth,
    bin_centers,
) = calc_jitter(ui, eye_bits, myPyBERT.pattern_len, ideal_xings, actual_xings)
print(f"Jitter (ps): ISI = {isi*1e12:5.2f}, DCD = {dcd*1e12:5.2f}, PJ = {pj*1e12:5.2f}, RJ = {rj*1e12:5.2f}")
plt.plot(np.array(bin_centers) * 1e12, hist_synth)
plt.title("Jitter PDF")
plt.xlabel("TIE (ps)")
plt.ylabel("p(TIE)")
plt.show()
MIN_BATHTUB_VAL = 1.0e-18
half_len = len(hist_synth) // 2
bathtub = list(np.cumsum(hist_synth[-1 : -(half_len + 1) : -1]))
bathtub.reverse()
bathtub = np.array(bathtub + list(np.cumsum(hist_synth[: half_len + 1])))
bathtub = np.where(
    bathtub < MIN_BATHTUB_VAL, 0.1 * MIN_BATHTUB_VAL * np.ones(len(bathtub)), bathtub
)  # To avoid Chaco log scale plot wierdness.
plt.plot(np.array(bin_centers) * 1e12, np.log10(bathtub))
plt.title("Bathtub Curve")
plt.xlabel("$\Delta t$ (ps)")
plt.ylabel("P[bit error] (log10)")
plt.axis(ymin=-15)
plt.grid()
plt.show()<jupyter_output><empty_output><jupyter_text>Final TouchesThe eye, jitter histogram, and bathtub curves above look great.One might think we're done.However, we've omitted two very important real World effects present in any serial communication link:1. random noise    Any real World link will have some amount of noise present that is completely independent of the data being sent. We typically model this sort of noise as being truly random, even though it really isn't.1. CDR induced jitter    When transmitting data serially, we don't transmit a separate clock.Instead, the receiver must "extract" the clock from the incoming data stream.The circuity that does this clock extraction often injects its own additional jitter into the system, and this must also be modeled.Let's add these final two finishing touches now...  Random NoisePyBERT has a parameter for setting the _standard deviation_ of the _random vertical noise_ that it will add to the Tx output signal: `rn`.Let's set that parameter to some non-zero value and see how it changes what we observe...<jupyter_code>myPyBERT.rn = 0.5  # std. dev. of random noise (V)
myPyBERT.console_log = ""
my_run_simulation(myPyBERT, initial_run=False, update_plots=False)
print(myPyBERT.console_log)

plotsig(myPyBERT.dfe_out)<jupyter_output><empty_output><jupyter_text>Note how the eye now looks much more like something we'd actually observe on a scope in the lab.Let's see how the jitter distribution has been affected...<jupyter_code>ideal_xings  = myPyBERT.ideal_xings
actual_xings = find_crossings(t, myPyBERT.ctle_out)
(   jitter,
    t_jitter,
    isi,
    dcd,
    pj,
    rj,
    jitter_ext,
    thresh,
    jitter_spectrum,
    jitter_ind_spectrum,
    spectrum_freqs,
    hist,
    hist_synth,
    bin_centers,
) = calc_jitter(ui, eye_bits, myPyBERT.pattern_len, ideal_xings, actual_xings)
print(f"Jitter (ps): ISI = {isi*1e12:5.2f}, DCD = {dcd*1e12:5.2f}, PJ = {pj*1e12:5.2f}, RJ = {rj*1e12:5.2f}")
plt.plot(np.array(bin_centers) * 1e12, hist_synth)
plt.title("Jitter PDF")
plt.xlabel("TIE (ps)")
plt.ylabel("p(TIE)")
plt.show()<jupyter_output>Jitter (ps): ISI = 17.57, DCD =  0.32, PJ =  0.00, RJ =  1.30<jupyter_text>Note how the jitter distribution now looks much more like a typical dual-Dirac distribution you may have seen displayed on a BERT scope.Also, note the non-zero value being reported for `RJ`.  (If you check above then you'll see that this value was previously being reported as zero.)  **Question:** Why is this value _not_ equal to: `0.5`, the value we assigned to `rn`?Finally, let's see how the bathtub curves have been affected by our addition of random noise...<jupyter_code>MIN_BATHTUB_VAL = 1.0e-18
half_len = len(hist_synth) // 2
bathtub = list(np.cumsum(hist_synth[-1 : -(half_len + 1) : -1]))
bathtub.reverse()
bathtub = np.array(bathtub + list(np.cumsum(hist_synth[: half_len + 1])))
bathtub = np.where(
    bathtub < MIN_BATHTUB_VAL, 0.1 * MIN_BATHTUB_VAL * np.ones(len(bathtub)), bathtub
)  # To avoid Chaco log scale plot wierdness.
plt.plot(np.array(bin_centers) * 1e12, np.log10(bathtub))
plt.title("Bathtub Curve")
plt.xlabel("$\Delta t$ (ps)")
plt.ylabel("P[bit error] (log10)")
plt.axis(ymin=-15)
plt.grid()
plt.show()<jupyter_output><empty_output><jupyter_text>Note that the "walls" of the bathtub curves now have a shape we're more accustomed to seeing, having lost their vertical "cliffs"; this is directly due to the addition of random noise.  CDR Induced JitterThe circuitry that extracts the original clock signal from the incoming symbol stream is called the _clock from data recovery_ (CDR) circuit. And it introduces some jitter of its own, which we must include in our analysis.Now, the exact relationship between this CDR induced jitter and that of the incoming symbol stream is quite complicated. And that makes a _correlated_ analysis beyond the scope of this simple treatment.So, instead, we offer a simpler _uncorrelated_ analysis, asking the reader to take it on faith that this leads to a conservative estimation of BER.An uncorrelated analysis of CDR induced jitter proceeds as follows:1. Assemble a _time interval error_ (TIE) track for the sampling instants produced by the CDR, just as we do for the zero crossings of the incoming symbol stream.1. Instead of decomposing the TIE, as we do for the incoming symbol stream crossings, just measure the _variance_ of this new TIE: $\sigma^2_{CDR}$.1. Model the CDR induced jitter as a _Gaussian random process_ having variance: $\sigma^2_{CDR}$.1. Repeat the jitter extrapolation and associated bathtub curve generation process, after _horizontally_ perturbing the crossing times of the incoming symbol stream with sampled values from our new "noise" generator.    Note the difference, here, wrt/ previous applications of "random" noise:    there, we were _vertically_ perturbing the samples of the incoming symbol stream.<jupyter_code>clk_times   = np.array(myPyBERT.clock_times)
valid_times = clk_times[np.where(clk_times > (t[-1] - ui*eye_bits))]
std_sig = myPyBERT.rj_dfe  # PyBERT's `rj_...` parameters hold std. dev.
var_sig = std_sig ** 2
var_cdr = np.var(np.diff(valid_times) - ui)
std_cdr = np.sqrt(var_cdr)
var_new = var_sig + var_cdr
std_new = np.sqrt(var_new)
print(f"Signal jitter std. dev. = {std_sig*1e12:5.2f} ps.")
print(f"CDR    jitter std. dev. = {std_cdr*1e12:5.2f} ps.")
print(f"New    jitter std. dev. = {std_new*1e12:5.2f} ps.")
new_xings = actual_xings + sp.stats.norm(scale=std_new).rvs(size=len(actual_xings))
(   jitter,
    t_jitter,
    isi,
    dcd,
    pj,
    rj,
    jitter_ext,
    thresh,
    jitter_spectrum,
    jitter_ind_spectrum,
    spectrum_freqs,
    hist,
    hist_synth,
    bin_centers,
) = calc_jitter(ui, eye_bits, myPyBERT.pattern_len, ideal_xings, new_xings)
print(f"Jitter (ps): ISI = {isi*1e12:5.2f}, DCD = {dcd*1e12:5.2f}, PJ = {pj*1e12:5.2f}, RJ = {rj*1e12:5.2f}")
plt.plot(np.array(bin_centers) * 1e12, hist_synth)
plt.title("Jitter PDF")
plt.xlabel("TIE (ps)")
plt.ylabel("p(TIE)")
plt.show()
MIN_BATHTUB_VAL = 1.0e-18
half_len = len(hist_synth) // 2
bathtub = list(np.cumsum(hist_synth[-1 : -(half_len + 1) : -1]))
bathtub.reverse()
bathtub = np.array(bathtub + list(np.cumsum(hist_synth[: half_len + 1])))
bathtub = np.where(
    bathtub < MIN_BATHTUB_VAL, 0.1 * MIN_BATHTUB_VAL * np.ones(len(bathtub)), bathtub
)  # To avoid Chaco log scale plot wierdness.
plt.plot(np.array(bin_centers) * 1e12, np.log10(bathtub))
plt.title("Bathtub Curve")
plt.xlabel("$\Delta t$ (ps)")
plt.ylabel("P[bit error] (log10)")
plt.axis(ymin=-15)
plt.grid()
plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10780283672184572, 'fraction_numerical': 0.028404251379476355, 'mean_word_length': 4.765157984628523, 'pattern_counts': {'":': 0, '<': 72, '<?xml version=': 0, '>': 70, 'https://': 3, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '197868', 'license': 'BSD-2-Clause', 'n_tokens_mistral': 10018, 'n_tokens_neox': 9249, 'n_words': 2765, 'path': 'misc/PyBERTasLibrary.ipynb', 'repo_name': 'eyecan/PyBERT', 'size': '27013'}