TT Lab
Get started
Learn Learning paths Courses

My Cable Has an Echo

Trace the notches of an unused branch line

Continue in TT Lab

Goal

Find the periodic notches of an open branch, and compare even the signal reduction when it is matched, using real AC responses. The prerequisites are the earlier signal integrity and pin loading labs, and Python functions, complex numbers, JSON, and file handling. The expected time is 85 minutes. Extend with +time before the default 60 minutes end (maximum 180 minutes). Files disappear when the session ends, so keep your source and report separately.

Why it matters

The length and termination of a wire create a different load at each frequency. Eliminating a notch does not mean you also preserved the signal magnitude. You learn not to guess the period from only the single deepest point, and not to report a resonance outside the range as if it were observed. It is an ideal single-mode lossless branch line and does not replace real-hardware measurement, parasitics, loss, or real bus certification.

Steps

  1. Save the whole implementation in /root/stub-notch/analyze.py. predict(config,hz) returns the theoretical H as a complex. config is a dict of exactly two keys, delay_ns=0.2/0.35/0.5/1 and termination=open/matched. delay_ns accepts only a non-bool int/float. hz is a finite positive non-bool int/float. Any other input is a ValueError. Do not change the original. open is H=0.5/(1+0.5j*tan(2πfTD)), and matched is 1/3. Convert TD from ns to seconds.
  2. relative_response(rows) takes a valid list of (Hz, real part, imaginary part) tuples and returns a list of (Hz, relative dB) tuples. Relative dB is 20log10(2*absolute value of the complex number). The no-branch H=0.5 is the fixed reference. A magnitude of 0 is a ValueError, and all samples and their order are preserved.
  3. notches(points,threshold_db=-15) finds interior local minima in a valid (Hz,dB) list with strictly increasing frequency and returns a list of hz, db objects. Include only bottoms that are lower than both neighbors and at or below the threshold, and exclude the endpoints. A continuous flat bottom of the same dB counts as one when it is lower than the outer neighbors on both sides, and you pick the first frequency. The result is in ascending frequency order.
  4. infer_delay(found) takes a valid list of notches in ascending frequency order. If there are fewer than two, None. Otherwise return spacing_hz, the median of the adjacent Hz spacings, delay_ns as 1e9/(2*spacing_hz), and max_relative_error, the maximum over all spacings of abs(spacing−median)/median. Do not compute it from the input settings or the absolute position of the first notch instead.
  5. band_min(points,low_hz,high_hz) returns the lowest dB in the closed band of interest of a valid (Hz,dB) table and its location as an hz, db object. Compare all samples inside the interval and the linearly interpolated Hz-versus-dB values at the two boundaries. If exactly tied, the lower frequency. If it is outside the table's range or low>=high, it is a ValueError. Even with no interior samples, you compare the two boundaries.
  6. analyze(folder) analyzes config.json and the response.tsv read with the provided simulator.read_response. Return the object in the notes below. Calculate the notches, the period, and the 10 to 200MHz worst value from the observed table. Find the maximum of the absolute difference between every sample's complex H and the predict result and judge whether it is 1e-8 or less. Even if it is a mismatch because only the settings changed, leave the original observation and do not change the raw file.
  7. campaign(values,folder) first validates the whole list of 1 to 8 non-duplicate configurations and creates a new folder. Run the provided simulator.capture(config,folder/case-NN) and analyze. NN is from 00 in order, and if the observed config differs from the request, it is a ValueError. It returns the results list, all_pass as the logical AND of all passed, model=lossless_parallel_stub, and measured=False, and saves the same object in report.json. Preserve the failed conditions and the four original files, and an existing folder is rejected even if empty. On a configuration error, it does not create the folder either.

Notes

The code is /root/stub-notch/analyze.py, and the provided helper is /opt/lab/fixtures/stub_notch/simulator.py. No extra installation, internet, or equipment is needed. capture(config,new_folder) returns a Path and leaves config.json, circuit.cir, response.tsv, and ngspice.log. read_response(path) checks the header, finite numbers, 10MHz to 2GHz, and 1MHz spacing of all 1991 samples and returns a list of (Hz, real part, imaginary part). A broken table is a ValueError. The far end of open is approximated with a 1 teraohm resistor, and all lines, the main line, and the source are 50Ω. What the helper calculates is only the circuit and the raw response, and you implement the notch and specification judgments.

The returned keys of step 6 are config, notches, periodic, band, max_complex_error, consistent, meets_spec, passed, and measured. notches is the detection result at the default −15dB, periodic is the infer_delay result, and band is the band_min(points,1e7,2e8) result. consistent is max_complex_error<=1e-8, meets_spec is band.db>=−1, and passed is whether both are True, with measured=False. This is a learning specification and not board certification. With one or no notches, periodic is None. Do not use the settings' delay instead or delete the observation.

After implementing the last step, run the real six conditions as follows. It is not a list in which all pass, so the normal report's all_pass=False. For a rerun, use a new name such as run-02.

cd /root/stub-notch
PYTHONPATH=/opt/lab/fixtures/stub_notch python3 - <<'PY'
from analyze import campaign
conditions = [dict(delay_ns=t, termination=m) for t in (0.2,0.5,1) for m in ('open','matched')]
print(campaign(conditions, '/root/stub-notch/run-01'))
PY

The source must be a regular file of at most 64KiB, and import must produce no output. Grading uses separate small samples and real engine conditions. The general numeric tolerance is 1e-8 relative and absolute, and the notch frequency is the Hz of the sample chosen, with no interpolation. For functions where the rules state that they take a valid numeric table, you need not make an extra parser. The grading process has limits of 12 seconds of CPU, 8MiB of files, and 35 seconds overall. It is different from the study time.

Calculate the branch load as a complex number

Save the whole implementation in /root/stub-notch/analyze.py. predict(config,hz) returns the theoretical H as a complex. config is a dict of exactly two keys, delay_ns=0.2/0.35/0.5/1 and termination=open/matched. delay_ns accepts only a non-bool int/float. hz is a finite positive non-bool int/float. Any other input is a ValueError. Do not change the original. open is H=0.5/(1+0.5j*tan(2πfTD)), and matched is 1/3. Convert TD from ns to seconds.

The load seen from the 50Ω source is the main-line 50Ω in parallel with the branch input. Even when matched, the branch load does not go away.

Use the same reference for all conditions

relative_response(rows) takes a valid list of (Hz, real part, imaginary part) tuples and returns a list of (Hz, relative dB) tuples. Relative dB is 20log10(2*absolute value of the complex number). The no-branch H=0.5 is the fixed reference. A magnitude of 0 is a ValueError, and all samples and their order are preserved.

If you divide by each response's maximum, the signal reduction of the matched branch disappears. Do not calculate the magnitude from the real part alone.

Detect the valleys one by one

notches(points,threshold_db=-15) finds interior local minima in a valid (Hz,dB) list with strictly increasing frequency and returns a list of hz, db objects. Include only bottoms that are lower than both neighbors and at or below the threshold, and exclude the endpoints. A continuous flat bottom of the same dB counts as one when it is lower than the outer neighbors on both sides, and you pick the first frequency. The result is in ascending frequency order.

Do not pick only the single deepest point. −15dB is also included in the threshold, and one that is low at both ends is not an observed interior notch.

Find the delay from the notch spacing

infer_delay(found) takes a valid list of notches in ascending frequency order. If there are fewer than two, None. Otherwise return spacing_hz, the median of the adjacent Hz spacings, delay_ns as 1e9/(2*spacing_hz), and max_relative_error, the maximum over all spacings of abs(spacing−median)/median. Do not compute it from the input settings or the absolute position of the first notch instead.

Distinguish the first notch's 1/(4TD) from the repeat spacing's 1/(2TD). Also keep the deviation of irregular spacings.

Find the weakest spot in the band of interest

band_min(points,low_hz,high_hz) returns the lowest dB in the closed band of interest of a valid (Hz,dB) table and its location as an hz, db object. Compare all samples inside the interval and the linearly interpolated Hz-versus-dB values at the two boundaries. If exactly tied, the lower frequency. If it is outside the table's range or low>=high, it is a ValueError. Even with no interior samples, you compare the two boundaries.

This grid is a linear frequency. If you copy the log-axis interpolation of the previous RC lab as is, you get a different result.

Distinguish no notch from passing the specification

analyze(folder) analyzes config.json and the response.tsv read with the provided simulator.read_response. Return the object in the notes below. Calculate the notches, the period, and the 10 to 200MHz worst value from the observed table. Find the maximum of the absolute difference between every sample's complex H and the predict result and judge whether it is 1e-8 or less. Even if it is a mismatch because only the settings changed, leave the original observation and do not change the raw file.

A matched branch has no notches, but you must separately check the fixed-reference gain of the band of interest. The complex model and the specification are separate conditions.

Compare length and termination conditions

campaign(values,folder) first validates the whole list of 1 to 8 non-duplicate configurations and creates a new folder. Run the provided simulator.capture(config,folder/case-NN) and analyze. NN is from 00 in order, and if the observed config differs from the request, it is a ValueError. It returns the results list, all_pass as the logical AND of all passed, model=lossless_parallel_stub, and measured=False, and saves the same object in report.json. Preserve the failed conditions and the four original files, and an existing folder is rejected even if empty. On a configuration error, it does not create the folder either.

Do not leave out a condition with a deep notch from the result, or treat matching as an unconditional success. Do not overwrite different run results into the same folder.