TDR: locate a boundary from polarity and round-trip delay
Goal
Recover one impedance boundary from the sign of the reflection and the round-trip delay, and leave the no-reflection case as a location that could not be observed.
The expected time is 80 minutes. Extend with +time before the default 60-minute session ends (maximum 180 minutes). Pod files disappear when it ends. Keep your source and results separately before it ends. The prerequisites are the earlier signal integrity lab and Python functions, lists, JSON, and file handling.
Why it matters
Even if the values look right, if you mix up the reflection sign, the time origin, and the round-trip interval, you end up reporting a different boundary. This lab guards against both the error of writing the location from the settings straight into the result and the error of producing a location from a waveform with no reflection. The circuit is an ideal single-mode lossless line, and it does not replace real measurement or certification.
Steps
- Write config(value) in analyze.py. Only the object keys lead_ns, tail_ohm, and rise_ns are allowed. Each value is one of 2/3/4, 25/50/100, and 0.2/0.6 respectively. Reject bool, strings, NaN, infinity, missing keys, and extra keys with a ValueError, and return a new dict without modifying the original. Do not run or print anything just on import.
- read_trace(path) reads a whitespace-separated TSV of at most 2MiB with the header time v(tx) and returns [(time_ns,voltage_v),...]. Both columns must be finite numbers, and time must be 0 or greater and strictly increasing. It needs at least 100 rows, a first time of 0, and a last time of 16ns (within an error of 1e-9ns). Corrupt or insufficient data is a ValueError, and the file is not modified. The time unit of the original file is s.
- plateaus(rows) takes a list of ns/V two-column rows and returns launch_v, the median of the closed window [2,3]ns, and settled_v, the median of [12,14]ns. Each window needs at least two points. There must be two or more rows, time 0 or greater and strictly increasing, and values that are finite non-boolean numbers, and insufficient or corrupt data is a ValueError. Do not change the original rows.
- reflection(launch_v,settled_v) returns gamma=(B-A)/A and z_ohm=50*(1+gamma)/(1-gamma). The inputs are finite numbers and bool is forbidden. If it is not A>0 and -1
- crossing(rows,threshold_v,after_ns,direction) returns, in ns, the first crossing time for direction = the integer 1 or -1. For rising, linearly interpolate the interval where a.vthreshold>=b.v, and pick the first event whose resulting time is >=after_ns. If there is none, None. rows has the same validity conditions as step 3, after_ns is finite and 0 or greater, and the threshold is a finite number. Invalid values and a bool direction are a ValueError.
- analyze(folder) reads config.json and trace.tsv and returns config, launch_v, settled_v, gamma, z_ohm, launch_ns, return_ns, distance_m, echo, consistent, simulated, and physical_certified. The departure is the A/2 rising crossing, and if there is a reflection, the return is the (A+B)/2 crossing (in the sign direction) with after=3ns. echo is |gamma|>1e-6, and if not, both the return and the distance are None. The distance is 0.2*(return-departure)/2 m. If there is no time, the distance is also None. Keep simulated=True and physical_certified=False. The agreement criteria follow the notes below.
- campaign(values,folder) pre-validates the whole list of 1 to 12 non-duplicate configurations and then creates a new folder. For each configuration, it runs the provided simulator.capture(config,folder/case-NN) and analyzes with analyze. NN is the two-digit input order starting from 00. If the config of an analysis result differs from the request, it is a ValueError. It returns a dict in which cases is the list of results in order, passed is whether all consistent are True, and measured is False. On a configuration error it does not create the output folder and does not overwrite an empty existing folder either.
- Write the CLI
analyze.py --configs <JSON 목록 경로> --out <새 폴더>(the placeholders are the path of the JSON list and the new folder). It runs campaign and prints the returned object as the same JSON to report.json and stdout. If passed=True, exit 0, and if False, exit 2, and the report is left. On configuration and file errors, exit with something other than 0 and leave the cause on stderr. Preserve each case-NN's config.json, circuit.cir, ngspice.log, and trace.tsv, and do not reuse existing results or an empty existing output folder.
Notes
Save the whole implementation in /root/tdr-location/analyze.py. No extra installation, internet, or real equipment is needed.
The provided helper is /opt/lab/fixtures/tdr_location/simulator.py, and it performs only config validation and circuit generation and real execution.
If you specify PYTHONPATH as below, the code's from simulator import capture works.
PYTHONPATH=/opt/lab/fixtures/tdr_location python3 /root/tdr-location/analyze.py --configs /opt/lab/fixtures/tdr_location/cases.json --out /root/tdr-location/run-01
The helper capture(config,new_folder) returns a Path and leaves config.json, circuit.cir, ngspice.log, and trace.tsv. It rejects an invalid configuration before running. It rejects an existing folder, so for a rerun use a new name such as run-02. The default cases.json has 9 conditions of TD 2/3/4ns × Z2 25/50/100Ω, with rise_ns=0.2.
The conditions of consistent in step 6 must all be satisfied at the same time. There is a departure time and the difference of A from 0.5V is 1e-6V or less, the difference between the observed gamma and (tail_ohm−50)/(tail_ohm+50) is 1e-6 or less, and echo equals tail_ohm!=50. If the settings imply a reflection, there are two crossings and the difference between the observed round-trip time and 2*lead_ns must be 0.02ns or less. If the settings imply no reflection, return_ns must be None. Even on a model mismatch, you leave the observed values and keep consistent=False.
The ngspice raw file is a two-column TSV time v(tx) with the time unit s. The times your analysis returns are in ns and the distance in m.
You only read the waveform and settings. analyze.py must be a regular file of at most 64KiB, and import must produce no output.
Grading runs a small separate sample and 4 real circuit conditions (including a slow edge), with a default numeric tolerance of 1e-8,
and the back-calculated impedance 1e-6Ω, the times 0.001ns, and the distance 0.001m. Booleans and None are distinguished exactly.
The grading process is limited to 12 seconds of CPU, 8MiB of files, and 35 seconds overall, and it cleans up child processes when it ends.
This execution limit differs from the study time. A forced termination or missing output is not a pass.
Fix the experimental conditions first
Write config(value) in analyze.py. Only the object keys lead_ns, tail_ohm, and rise_ns are allowed. Each value is one of 2/3/4, 25/50/100, and 0.2/0.6 respectively. Reject bool, strings, NaN, infinity, missing keys, and extra keys with a ValueError, and return a new dict without modifying the original. Do not run or print anything just on import.
bool is a subtype of int in Python. Check the type explicitly and confirm that every field is a finite allowed number.
Convert seconds to ns and reject a truncated waveform
read_trace(path) reads a whitespace-separated TSV of at most 2MiB with the header time v(tx) and returns [(time_ns,voltage_v),...]. Both columns must be finite numbers, and time must be 0 or greater and strictly increasing. It needs at least 100 rows, a first time of 0, and a last time of 16ns (within an error of 1e-9ns). Corrupt or insufficient data is a ValueError, and the file is not modified. The time unit of the original file is s.
First check the header and the row length. Do the 1e9 conversion only once, and reject duplicate times too. If you do not check the last row, a partial output looks like a complete result.
Read the two plateaus in different windows
plateaus(rows) takes a list of ns/V two-column rows and returns launch_v, the median of the closed window [2,3]ns, and settled_v, the median of [12,14]ns. Each window needs at least two points. There must be two or more rows, time 0 or greater and strictly increasing, and values that are finite non-boolean numbers, and insufficient or corrupt data is a ValueError. Do not change the original rows.
It is not the mean of the whole waveform or the mean of the two end values. Include the samples on the window boundaries too, and follow the median convention for both odd and even sample counts.
Convert even a negative reflection back to impedance
reflection(launch_v,settled_v) returns gamma=(B-A)/A and z_ohm=50*(1+gamma)/(1-gamma). The inputs are finite numbers and bool is forbidden. If it is not A>0 and -1
B/A is a ratio that includes the original forward wave. You must separate the reflected component with B-A, and do not erase a negative reflection with an absolute value.
Interpolate the rising edge and the falling edge
crossing(rows,threshold_v,after_ns,direction) returns, in ns, the first crossing time for direction = the integer 1 or -1. For rising, linearly interpolate the interval where a.vthreshold>=b.v, and pick the first event whose resulting time is >=after_ns. If there is none, None. rows has the same validity conditions as step 3, after_ns is finite and 0 or greater, and the threshold is a finite number. Invalid values and a bool direction are a ValueError.
Even if after_ns is inside the interval, it is valid if the interpolated result satisfies the condition. A case where the starting point is already high, or the plateau itself, is not counted as a new crossing.
Read the location of the boundary from the real waveform
analyze(folder) reads config.json and trace.tsv and returns config, launch_v, settled_v, gamma, z_ohm, launch_ns, return_ns, distance_m, echo, consistent, simulated, and physical_certified. The departure is the A/2 rising crossing, and if there is a reflection, the return is the (A+B)/2 crossing (in the sign direction) with after=3ns. echo is |gamma|>1e-6, and if not, both the return and the distance are None. The distance is 0.2*(return-departure)/2 m. If there is no time, the distance is also None. Keep simulated=True and physical_certified=False. The agreement criteria follow the notes below.
Do not use the settings' lead_ns as the distance as is; use the two observed crossing times. In the no-reflection case, even if the settings have a boundary, do not produce an observed location.
Validate all settings and then run
campaign(values,folder) pre-validates the whole list of 1 to 12 non-duplicate configurations and then creates a new folder. For each configuration, it runs the provided simulator.capture(config,folder/case-NN) and analyzes with analyze. NN is the two-digit input order starting from 00. If the config of an analysis result differs from the request, it is a ValueError. It returns a dict in which cases is the list of results in order, passed is whether all consistent are True, and measured is False. On a configuration error it does not create the output folder and does not overwrite an empty existing folder either.
If you validate and run only the first configuration, a partial result is produced when a later one is wrong. Connect the provided module path with PYTHONPATH, and compare the configuration in the observed folder with the request again.
Leave the report and the original evidence
Write the CLI analyze.py --configs <JSON 목록 경로> --out <새 폴더> (the placeholders are the path of the JSON list and the new folder). It runs campaign and prints the returned object as the same JSON to report.json and stdout. If passed=True, exit 0, and if False, exit 2, and the report is left. On configuration and file errors, exit with something other than 0 and leave the cause on stderr. Preserve each case-NN's config.json, circuit.cir, ngspice.log, and trace.tsv, and do not reuse existing results or an empty existing output folder.
Put the execution code under a name guard. If you output only the report and discard the original waveforms, you cannot review it again. Do not turn an exception into a success.