TT Lab
Get started
Learn Learning paths Courses

My Cable Has an Echo

Bandwidth lost to pin capacitance

Continue in TT Lab

Goal

Extract the relative bandwidth and phase from the real AC response of a lumped RC circuit and compare six conditions. You need Python functions, complex numbers, lists, JSON, and voltage-divider basics. The expected time is 80 minutes, so extend with +time before the default 60 minutes end (maximum 180 minutes). Files disappear when the session ends, so keep them separately.

Why it matters

You learn the frequency dependence you miss when you view the load of an input pin as only a resistor. You distinguish the absolute voltage division from the attenuation relative to the passband, magnitude from phase, and a successful run from passing a specification. It is an ideal lumped RC model and does not replace a long transmission line, a nonlinear pin, real-hardware measurement, or bus specification certification.

Steps

  1. Save the whole implementation in /root/pin-bandwidth/analyze.py. predict(config) returns dc_gain, rth_ohm, and pole_hz. The configuration is a dict of exactly three keys, one each of source_ohm=10/25/50, load_ohm=50/100, and cap_pf=10/20/40. It accepts only numeric int/float, and bool, strings, missing keys, extra keys, and out-of-range values are a ValueError. Do not change the original. Calculate the DC division, the parallel equivalent resistance, and 1/(2πRthC).
  2. read_response(path) reads a whitespace-separated file with the header frequency hre him and returns a list of (Hz, real part, imaginary part) tuples. Check a regular file of at most 1MiB, 2 or more data rows, 3 finite numbers per row, and positive, strictly increasing frequencies, and reject violations with a ValueError. Do not convert to seconds or MHz. Whitespace before and after the header is allowed.
  3. metrics(rows) takes a valid list of (Hz, real part, imaginary part) and returns a list of objects with hz, magnitude, db, and phase_deg for each row. The magnitude is the absolute value of the complex number, db is 20log10(magnitude), and the phase is the atan2 in degrees, in the range −180 to 180 degrees. A magnitude of 0 is a ValueError. Keep all the input rows and their order.
  4. bandwidth(points,dc_gain) takes a valid metrics result and a positive DC gain. The threshold is 20log10(dc_gain)−10log10(2). In the first adjacent interval where a.db > threshold >= b.db, linearly interpolate dB against log10(Hz) and return Hz. If there is no crossing, None, and do not regard being at or below the threshold from the start as a crossing.
  5. at_frequency(rows,hz) returns the complex value at hz from a valid complex response table. If it is an exact sample, use that value, and if between samples, linearly interpolate the real and imaginary parts separately by the ratio on the log(frequency) axis. If it is outside the table's range, it is a ValueError. It is not a function that interpolates magnitude or phase separately.
  6. analyze(folder) reads config.json and response.tsv and returns the result object in the notes below. Check the whole 161-row, 1MHz to 10GHz, 40-points-per-decade grid, and if it is insufficient or different, it is a ValueError (frequency relative error 1e-9). Get the bandwidth and the relative gain and phase at 100MHz from the table, and compare every complex sample with H0/(1+jf/fc). Even on a model mismatch, the observed values are left.
  7. campaign(values,folder) validates the whole list of 1 to 18 non-duplicate configurations with predict and then creates a new folder. Run each condition with the provided simulator.capture(config,folder/case-NN) and analyze it. NN is from 00 in input order, and if the observed config differs from the request, it is a ValueError. It returns the results list, all_pass indicating whether all passed are True, model=lumped_rc, and measured=False, and saves the same object in report.json. An existing folder is rejected even if empty. On a configuration error, it does not create the folder either.

Notes

The implementation path is /root/pin-bandwidth/analyze.py. No internet, extra packages, or equipment are needed. The provided helper /opt/lab/fixtures/pin_bandwidth/simulator.py only generates the circuit and runs real ngspice. It applies AC 1V to a circuit with Rs in series and RL and C in parallel, and calculates 1MHz to 10GHz at 40 points per decade. capture(config,new_folder) returns a Path and does not overwrite existing results. The input C unit is pF.

The returned keys of step 6 are config, bandwidth_hz, relative_db, phase_deg, max_complex_error, consistent, meets_spec, passed, and measured. relative_db is, at 100MHz, 20log10(|H|/H0), and phase_deg is the phase of the same interpolated H. max_complex_error is the maximum over all rows of the absolute difference between the observed H and the model H. consistent is True only when this error is 1e-8 or less, the observed bandwidth exists, and its error relative to the predicted fc is 0.2% or less. meets_spec is True when the observed bandwidth is 200MHz or more, relative_db is −0.5dB or more, and phase_deg is −20 degrees or more all hold at the same time. passed is whether both consistent and meets_spec are True, and measured is False. If there is no crossing and the bandwidth is None, consistent, meets_spec, and passed are False. The observation itself is not discarded.

After implementing step 7, run the six conditions of source 25/50Ω × C 10/20/40pF with a 50Ω load using the following command. Failing conditions are included, so the normal report's all_pass is False. For a rerun, use a new output folder name.

cd /root/pin-bandwidth
PYTHONPATH=/opt/lab/fixtures/pin_bandwidth python3 - <<'PY'
from analyze import campaign
conditions = [dict(source_ohm=r, load_ohm=50, cap_pf=c) for r in (25,50) for c in (10,20,40)]
report = campaign(conditions, '/root/pin-bandwidth/run-01')
print(report)
PY

The source must be a regular file of at most 64KiB, and import must produce no output. Only read the waveforms and settings. Grading uses separate small samples and the real engine, and the general numeric tolerance is 1e-8 relative and absolute. The grading process has limits of 12 seconds of CPU, 8MiB of output files, and 35 seconds overall. It is different from the study time.

Predict the division and the pole

Save the whole implementation in /root/pin-bandwidth/analyze.py. predict(config) returns dc_gain, rth_ohm, and pole_hz. The configuration is a dict of exactly three keys, one each of source_ohm=10/25/50, load_ohm=50/100, and cap_pf=10/20/40. It accepts only numeric int/float, and bool, strings, missing keys, extra keys, and out-of-range values are a ValueError. Do not change the original. Calculate the DC division, the parallel equivalent resistance, and 1/(2πRthC).

Convert pF to F, and think about the two resistors the capacitor sees when the voltage source is set to 0.

Read the complex response table

read_response(path) reads a whitespace-separated file with the header frequency hre him and returns a list of (Hz, real part, imaginary part) tuples. Check a regular file of at most 1MiB, 2 or more data rows, 3 finite numbers per row, and positive, strictly increasing frequencies, and reject violations with a ValueError. Do not convert to seconds or MHz. Whitespace before and after the header is allowed.

Also reject the case where two adjacent rows have the same frequency. The analysis does not modify the raw file.

Separate magnitude and phase

metrics(rows) takes a valid list of (Hz, real part, imaginary part) and returns a list of objects with hz, magnitude, db, and phase_deg for each row. The magnitude is the absolute value of the complex number, db is 20log10(magnitude), and the phase is the atan2 in degrees, in the range −180 to 180 degrees. A magnitude of 0 is a ValueError. Keep all the input rows and their order.

The real part is different from the magnitude. Check by hand the magnitude of 3+4j and the quadrants of negative real and imaginary parts.

Find the relative half-power frequency

bandwidth(points,dc_gain) takes a valid metrics result and a positive DC gain. The threshold is 20log10(dc_gain)−10log10(2). In the first adjacent interval where a.db > threshold >= b.db, linearly interpolate dB against log10(Hz) and return Hz. If there is no crossing, None, and do not regard being at or below the threshold from the start as a crossing.

It is not an absolute −3dB. If the dB difference is half between 1MHz and 100MHz, the frequency is 10MHz.

Compare complex numbers at the same frequency

at_frequency(rows,hz) returns the complex value at hz from a valid complex response table. If it is an exact sample, use that value, and if between samples, linearly interpolate the real and imaginary parts separately by the ratio on the log(frequency) axis. If it is outside the table's range, it is a ValueError. It is not a function that interpolates magnitude or phase separately.

First find the position ratio on the log axis, and then move between the two complex numbers. The two endpoints are not extrapolation.

Verify the observation and the specification separately

analyze(folder) reads config.json and response.tsv and returns the result object in the notes below. Check the whole 161-row, 1MHz to 10GHz, 40-points-per-decade grid, and if it is insufficient or different, it is a ValueError (frequency relative error 1e-9). Get the bandwidth and the relative gain and phase at 100MHz from the table, and compare every complex sample with H0/(1+jf/fc). Even on a model mismatch, the observed values are left.

To catch a wrong phase with the same magnitude, you must compare the absolute value of the complex difference. Separate the specification judgment from the model agreement.

Preserve the evidence of the six conditions

campaign(values,folder) validates the whole list of 1 to 18 non-duplicate configurations with predict and then creates a new folder. Run each condition with the provided simulator.capture(config,folder/case-NN) and analyze it. NN is from 00 in input order, and if the observed config differs from the request, it is a ValueError. It returns the results list, all_pass indicating whether all passed are True, model=lumped_rc, and measured=False, and saves the same object in report.json. An existing folder is rejected even if empty. On a configuration error, it does not create the folder either.

Leave failed conditions in results too. Do not delete the config.json, circuit.cir, response.tsv, and ngspice.log that capture made.