TT Lab
Get started
Learn Learning paths Courses

Electronics Foundations — Validating Sensor Inputs

Will Adding a Second RC Stage Solve It?

Continue in TT Lab

Goal

Compare filters for the greenhouse sensor by order, loading, frequency, and time response, and build a verification CLI that preserves real ngspice data.

Why it matters

Proceed after knowing the earlier aliasing unit and Python functions, files, complex numbers, lists, dicts, and exceptions. The lab is expected to take 120 minutes, so extend with +time before the default 60 minutes ends. The maximum is 180 minutes, and files disappear when the session ends. Keep the code and report you need separately. No real equipment or physical voltage is handled.

Environment and files

The deliverable is /root/filter-response/analyze.py. Python3 and ngspice42 are in the image, and no external downloads or added permissions are needed. If you add /opt/lab/fixtures/filter_response to sys.path, you can import simulator. The provided capture(cfg,new_folder) runs the real circuit and returns the output folder Path. It leaves input.cir, config.json, solver.log, ac.tsv, pass.tsv, stop.tsv, and step.tsv inside. An existing folder is not overwritten, and the parent must already exist.

The circuits are exactly the four structures in the reading. The AC input is an amplitude of ac_v and a phase of 23 degrees. The step input is 0V from 0 to 1ms, rises to 1V over the next 1µs, and holds until 50ms. The ideal buffer is a controlled voltage source with infinite bandwidth and unlimited output, and it is not a real operational amplifier.

Configuration contract

config accepts only a dict with exactly the 5 keys below. Numbers accept only int and float, and bool, strings, NaN, infinity, and exceeding the float conversion range are rejected with a ValueError. Numbers are returned as float, topology stays str, and where there is no Q, None is kept.

Key Allowed values
topology one of rc1, rc2-loaded, rc2-buffered, sk2
f0_hz 220 or 300
q for sk2, one of 0.5, 1/math.sqrt(2), 1.4; for other circuits only None
dt_s 5e-6 or 2.5e-6
ac_v one of 0.5, 1, 2

f0_hz is a frequency scale and does not mean the −3dB frequency of every circuit. R=1000Ω and C=1/(2πRf0_hz). Only sk2 uses C1=2qC and C2=C/(2q), and the others use the same R and C as in the reading. q=1/sqrt(2) is recorded in JSON as 0.7071067811865475.

AC file and Bode contract

The kind of read_ac is one of sweep, pass, and stop. The file is a regular file of at most 1MiB, and the header is, separated by whitespace, frequency v(in) v(in) v(out) v(out). Blank lines are ignored. Each data row has 5 finite floats, voltage component absolute value ≤1e6, and complex input magnitude ≥1e-12. The frequencies must be positive and strictly increasing. Content errors are a ValueError, and OS file errors may be passed through.

The first frequency must be within 1e-8Hz of the stated value and the last within 1e-7Hz. Every interior frequency must equal the specified grid within a relative 1e-10 or an absolute 1e-9. Do not check only the start and end. The return is a list of 5-float tuples.

bode accepts validated rows and does not change the original. H=complex(output real,output imaginary)/complex(input real,input imaginary). The keys of each returned dict are frequency_hz, h_re, h_im, gain=abs(H), db=20log10(gain), and phase_deg=degrees(atan2(H.imag,H.real)). If gain=0, db and phase_deg are None. The list order is preserved.

Model and step observation contract

transfer validates the settings with config and accepts a finite number 0≤freq≤1e6. With p=1j*freq/f0_hz, it returns as complex rc1:1/(1+p), rc2-loaded:1/(1+3p+p²), rc2-buffered:1/(1+p)², and sk2:1/(1+p/q+p²).

The provided simulator.load_step(path) validates a file with the header time v(in) v(out), at most 8MiB and 2 to 100000 data rows, and returns a list of 3-column tuples. Every value is finite, the absolute voltage is ≤1e6, and time is 0 or greater and strictly increasing. The first time must be ≤1µs, and the last within 1e-12 seconds of 50ms. Content errors are a ValueError. A student does not need to reimplement this loader.

measure_step uses the loader and linearly interpolates the output voltage at t=0.001001+i1e-5 (i=0..4899). An exact row is read as is, and extrapolation is a ValueError. The returned keys are samples_v (4900 voltages), overshoot_v=max(0,max(samples_v)−1), settled_at_s, grid_step_s=1e-5, and observation_end_s=0.001001+48991e-5.

settled_at_s is a time relative to the completion of the rise at 1.001ms. It returns the index after the last sample with |voltage−1|>.02 multiplied by 1e-5. If even the last sample is outside, None, and if all are inside, 0. This is a judgment on the observation grid and finite observation interval, and is not a proof of the continuous-time maximum or of stability for infinite time.

Judgment and analysis contract

assess accepts a normal configuration, the list from bode for sweep, the wanted dict at 125Hz, the nuisance dict at 875Hz, and the step dict from measure_step. It is assumed that the intermediate reports passed in follow the contract above, and you do not need to repeat the full structure validation. The returned keys are as follows.

analyze(value,folder) connects config, read_ac, bode, measure_step, and assess. The return is schema=1, config, sweep, wanted, nuisance, step, and assessment. Read ac.tsv as sweep and pass.tsv and stop.tsv as pass and stop respectively, and use the first entries as wanted and nuisance. Do not change the original files and original configuration, and pass errors on.

Condition comparison and CLI contract

campaign(values,runner) first validates all of an exact list of 2 to 6 configurations. The input dt_s must all be 5e-6. For each condition, runner(copy of the default settings)→analyze, then runner→analyze with new settings in which only dt_s is changed to 2.5e-6. Finish one condition and then run the next. Exceptions from runner are passed on, and the original input is not changed.

The return is schema=1, runs, candidate_indices, and scope='ideal-linear-filters-and-finite-step-observation'. runs is in input order and holds coarse and fine (each an analyze report), difference_v (the maximum absolute difference of the samples at the same 4900 times), and converged (difference≤1e-5V). candidate_indices is the list of 0-based indices of the conditions that converged and for which both assessment.all_pass are True. An empty candidate list is a normal result, and distinguish it from an execution error.

main(argv) takes a settings-list JSON file and the path of an output folder that does not exist yet. After checking every configuration in the JSON and the default dt, it creates the folder. The parent must already exist. In the order of runner calls, call the provided capture in new subfolders run-00, run-01, .... Print the result to stdout as one line of JSON and return 0. For an argument, configuration, file, or execution Exception, return one line {"error":"analysis_failed"} and 2, and do not print a traceback. Do not delete new evidence already created even on failure. On import, do not run the CLI, and exit with main's return value only when run directly.

The provided settings are /opt/lab/fixtures/filter_response/cases.json. In the final step, run it with a new name as below and check the original files along with the report. Choose a redirect path that does not overwrite a previous report either.

python3 /root/filter-response/analyze.py /opt/lab/fixtures/filter_response/cases.json /root/filter-response/trial-01 > /root/filter-response/report-01.json

Steps

  1. Validate the circuit conditions first: implement config(value) in /root/filter-response/analyze.py. Validate the 5-key settings below and return a new dict with the numbers normalized to float. An invalid value is a ValueError, and the original input is not changed.
  2. Read the five columns of the complex voltage: add read_ac(path,kind="sweep"). After checking the file and grid contract below, return a list of tuples of frequency, input real part, input imaginary part, output real part, and output imaginary part.
  3. Remove the input amplitude and phase: add bode(rows). From each validated row, compute the complex output/input and return a list of frequency, complex gain, magnitude, dB, and phase in the format below.
  4. Calculate the transfer function including loading: add transfer(cfg,freq). Validate the settings and frequency and return the transfer functions of the four circuits in the reading as complex.
  5. Find the last departure from the band: add measure_step(path). Read the original waveform with the provided load_step and linearly interpolate the output voltage at the specified 4900 times. Return the overshoot, the settling time, the observation grid, and the samples.
  6. Judge the frequency and time requirements together: add assess(cfg,sweep,wanted,nuisance,step). Calculate separately the complex model error at every AC point, the gain at the two required frequencies, and the step overshoot and settling requirements, and return the final AND.
  7. Build a verification report from files: add analyze(value,folder). Read the configuration, the 3 AC files, and the 1 step file, and connect the full analysis and judgment. Pass errors on instead of turning them into a success report.
  8. Connect the two calculation precisions to a real CLI: add campaign(values,runner), main(argv), and the CLI entry point. After validating all configurations, run each circuit at 5µs and 2.5µs and choose only the conditions that satisfy convergence and both design judgments.

Notes

Grading separates the pure-function checks on temporary files from the real ngspice and separate-CLI runs. It does not change the student's code and folder. If you hardcode the numeric table, it fails for other input amplitudes, frequencies, and loading conditions. This selection is a candidate within an ideal linear model. The real amplifier's supply, bandwidth, slew rate, noise, part tolerances, and reliability across temperature need separate verification.

Validate the circuit conditions first

Implement config(value) in /root/filter-response/analyze.py. Validate the 5-key settings below and return a new dict with the numbers normalized to float. An invalid value is a ValueError, and the original input is not changed.

The meaning of Q differs by circuit type. Do not accept strings or bool as numbers.

Read the five columns of the complex voltage

Add read_ac(path,kind="sweep"). After checking the file and grid contract below, return a list of tuples of frequency, input real part, input imaginary part, output real part, and output imaginary part.

The reason the same header appears twice is complex numbers. Even if the start and end match, the middle of the grid can be wrong.

Remove the input amplitude and phase

Add bode(rows). From each validated row, compute the complex output/input and return a list of frequency, complex gain, magnitude, dB, and phase in the format below.

Check 20log10 and the quadrant of atan2. Do not leave the input phase of 23 degrees in the filter phase.

Calculate the transfer function including loading

Add transfer(cfg,freq). Validate the settings and frequency and return the transfer functions of the four circuits in the reading as complex.

For a direct connection, the p coefficient of the denominator is 3. Distinguish it from the 2 of the case separated by a buffer.

Find the last departure from the band

Add measure_step(path). Read the original waveform with the provided load_step and linearly interpolate the output voltage at the specified 4900 times. Return the overshoot, the settling time, the observation grid, and the samples.

What matters is whether it went out again after the time it first entered the band. If it is outside through the last sample, null.

Judge the frequency and time requirements together

Add assess(cfg,sweep,wanted,nuisance,step). Calculate separately the complex model error at every AC point, the gain at the two required frequencies, and the step overshoot and settling requirements, and return the final AND.

A response with only the magnitude right and the phase wrong is also a model disagreement. An implementation that skips time when just the two AC points pass is wrong.

Build a verification report from files

Add analyze(value,folder). Read the configuration, the 3 AC files, and the 1 step file, and connect the full analysis and judgment. Pass errors on instead of turning them into a success report.

Preserve the original waveforms and leave the intermediate results in the report as well, so that the calculation can be traced.

Connect the two calculation precisions to a real CLI

Add campaign(values,runner), main(argv), and the CLI entry point. After validating all configurations, run each circuit at 5µs and 2.5µs and choose only the conditions that satisfy convergence and both design judgments.

Distinguish an execution failure from an empty candidate list. Do not overwrite an existing evidence folder, and check the real call together with the stored report.