Electronics Foundations — Validating Sensor Inputs
The Greenhouse Sensor Remembers the Previous Channel
Goal
Analyze, with real ngspice waveforms, the phenomenon of a space greenhouse ADC remembering the previous channel's value, and build a comparison report that distinguishes error, model, and convergence.
Why it matters
Proceed after finishing the earlier DC loading, RC, and ADC units. It uses Python functions, lists, dicts, exceptions, file reading, and exponential functions. No voltage is applied to real equipment. The expected time is 120 minutes, so extend with +time before it expires. The maximum is 180 minutes, and files disappear when the session ends. Keep the code and report you need separately.
Environment and deliverables
The deliverable is /root/adc-acquisition/analyze.py. Python3 and ngspice42 are in the image. No external downloads or added permissions are needed. The provided capture(cfg,new_folder) in /opt/lab/fixtures/adc_acquisition/simulator.py actually runs the fixed circuit and returns the path new_folder/trace.tsv. It also leaves input.cir, config.json, and solver.log in the same folder and does not overwrite an existing folder. No answers for the analysis or judgment are provided.
The waveform generation helper uses the same input-switch model as the circuit in the reading. The first input switches linearly to low_v between 1.8µs and 1.801µs from high_v. The control pulse starts at 100ns, with rise and fall of 1ns, a high-level width of acq_s, and a period of 1µs. Ron is 100Ω, Roff is 10¹²Ω, the threshold is 0.5V, and the initial sample voltage is 0V. high_v is just a name for the first channel and may be lower than low_v.
Configuration contract
config is an exact dict and accepts only the 8 keys below. Every number rejects bool, strings, NaN, and infinity. bits accepts an exact int, and the others accept int or float and return it as float.
| Key | Unit and allowed range |
|---|---|
| rs_ohm | Ω, 100 to 10000 inclusive |
| cap_f | F, 10e-12 to 40e-12 inclusive |
| acq_s | s, 100e-9 to 350e-9 inclusive |
| dt_s | s, 5e-12 or 2.5e-12 |
| high_v·low_v | V, each 0 to vref_v inclusive, and the two values differ from each other |
| vref_v | V, 1 to 5 inclusive; for converting code width, not a circuit supply model |
| bits | an actual int, 8 to 16 inclusive; for converting code width |
Waveform and observation contract
read_trace accepts a regular file, at most 256MiB, with 2 to 2,000,000 data rows excluding blank lines. The header is, separated by whitespace, time v(vin) v(gate) v(hold) in that order. Each row has 4 finite floats, the time must be 0 or greater and strictly increasing, the first time at most 1ns, and the last time equal to 3.5µs within an absolute 1e-15 seconds. Invalid content is a ValueError, and OS file errors may be passed through. For sample on validated points, you do not need to repeat the full validation on every call.
The observation time of observe, for k=0..3, is 100e-9+k*1e-6+acq_s+27e-9 seconds. The target list is high_v,high_v,low_v,low_v. The end of each hold is min((k+1)*1e-6+50e-9,3.5e-6) seconds. The returned keys are times_s (the four times), targets_v (the four targets), samples_v (the four stored voltages), and hold_drift_v (the maximum absolute change from each observation to the end of the hold). If the input, gate, or hold conditions are violated, it is a ValueError. It does not modify the waveform or the configuration.
Prediction and judgment contract
predict carries forward Vnew=Vtarget+(Vold−Vtarget)*exp(−(acq_s+1e-9)/((rs_ohm+100)*cap_f)) from the previous result. The start is Vold=0, and the targets are the 4 above. The finite leakage while off is ignored in this analytical formula.
assess uses only the samples_v and targets_v of observed. Check that they are 4 non-bool finite numbers and the target list in the configuration. The returned keys are max_model_error_v, model_consistent, error_lsb, decision, and all_pass. model_error is the maximum absolute difference between the observation and predict, and model_consistent is a bool for whether it is at most 100µV. error_lsb is the 4 values of each |observation−target| divided by vref_v/(2**bits).
If the model disagrees, all 4 decisions are invalid_model. If it agrees, then for each absolute voltage error e, pass if e+10µV≤0.5LSB, fail if e−10µV>0.5LSB, and borderline otherwise. all_pass is True only when all four are pass. The 10µV is this experiment's decision-margin band and not a certified value of real measurement uncertainty.
Condition comparison and CLI contract
campaign accepts a list of exactly 2 to 6 configurations, validates all of them, and then runs. Every input dt_s must be 5e-12. runner(cfg) is a provided call that returns a waveform file path. For each condition, run runner→analyze with a copy of the original configuration, then runner→analyze with a copy in which only dt_s is changed to 2.5e-12. Finish both runs of one condition before running the next condition, and do not modify the original input. Execution exceptions are passed on to the caller.
It returns schema=1, runs, candidate_indices, and scope='ideal-switched-rc-only'. runs is in input order, and each entry has coarse and fine (each an analyze report), difference_v (the maximum absolute difference of the four stored voltages), and converged (a bool for difference≤10µV). candidate_indices is the list of 0-based indices of the conditions that converged and for which both all_pass values are True. If there is no candidate, it is an empty list, and do not confuse it with an error. If different variables were changed at the same time, do not assert the effect of each cause from this list alone.
main(argv) takes two arguments: a settings-list JSON file and the path of an output folder that does not exist yet. After validating every configuration in the JSON and the default dt, it creates the output folder (the parent must already exist). Put /opt/lab/fixtures/adc_acquisition on sys.path and import simulator.capture. For every runner call, call capture with a new subfolder of output_folder/run-00, run-01, .... Print the campaign 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 created before the failure. On import, do not run the CLI, and exit with main's return value only when run directly.
The provided example list for step 8 is /opt/lab/fixtures/adc_acquisition/cases.json. To check directly, run it as below with a new folder name, and look at input.cir, config.json, solver.log, and trace.tsv along with the report. Choose a name for the report file that does not overwrite existing material either.
python3 /root/adc-acquisition/analyze.py /opt/lab/fixtures/adc_acquisition/cases.json /root/adc-acquisition/trial-01 > /root/adc-acquisition/report-01.json
Steps
- State numbers and units explicitly: implement config(value). Check the type and range of the 8 keys below and return a new dict without modifying the original. Only bits is normalized to int and the rest to float, and an invalid value is a ValueError.
- Read only a waveform generated to the end: read_trace(path) checks the TSV contract below and returns a list of tuples of four floats. Check the UTF-8, whitespace-separated header, rows, size, finite numbers, time order, and start/end range. Ignore blank lines. Do not fix the waveform.
- Interpolate the observation time: sample(points,when,col=3) linearly interpolates that time in a validated waveform. col is an actual int 1, 2, or 3, when is a non-bool finite number, and both endpoints are allowed. A time outside the range and a wrong column are a ValueError.
- Distinguish input, switch, and stored voltage: observe(cfg,points) validates with config and then calculates the four observation times and target voltages. At observation, the absolute value of gate and the difference of vin from the target must each be at most 1µV. The maximum difference between the sample voltage and the voltage at the end of the hold must also be at most 1µV. It returns the observation dict below.
- Carry over the previous acquisition voltage: predict(cfg) validates the configuration and then returns a list of 4 voltages using a recursive formula that acquires the first channel twice and the second channel twice from an initial 0V. The connection time is acq_s+1ns and the resistance is rs_ohm+100Ω.
- Separate error, model, and boundary judgments: assess(cfg,observed) returns the LSB error, the maximum difference from the analytical formula, model agreement, the four sample judgments, and all_pass according to the judgment contract below. The samples_v and targets_v of the observation dict are 4 finite numbers, and if the target list differs from the configuration, it is a ValueError.
- Build a report from a real file: analyze(cfg,trace_path) connects config→read_trace→observe→assess and returns a dict with the four keys schema=1, config, observation, and assessment. File corruption is passed on, not turned into a success report.
- Connect the condition comparison to a real CLI: implement campaign(configs,runner), main(argv), and the CLI entry point under the contract below. Validate all configurations first, analyze the 5ps and 2.5ps waveforms for each condition, and check convergence together with both judgments. It is checked with real ngspice and a separate CLI process.
Notes
Grading checks the functions with separate waveforms in a temporary folder, and the last step also runs real ngspice and a separate CLI process. It does not change the student folder and cleans up only the check-only folder. If you return just the example numbers, it fails for other voltage directions and conditions. It handles waveforms of about 700,000 to 1,400,000 rows, so do not launch several runs at once. Keep one output folder and review the results.
You pick candidates only within the ideal switched RC model. The real ADC's charge injection, noise, protection circuit, temperature, nonlinearity, and the stability of a real buffer are not verified. It is not a task that connects equipment or applies physical voltage.
State numbers and units explicitly
Implement config(value). Check the type and range of the 8 keys below and return a new dict without modifying the original. Only bits is normalized to int and the rest to float, and an invalid value is a ValueError.
A bool behaves like an int too, but you must not accept it as a circuit's resistance value. Unify the units to seconds, farads, and ohms before calculating.
Read only a waveform generated to the end
read_trace(path) checks the TSV contract below and returns a list of tuples of four floats. Check the UTF-8, whitespace-separated header, rows, size, finite numbers, time order, and start/end range. Ignore blank lines. Do not fix the waveform.
Do not judge that a waveform is complete from the exit code or the existence of the file alone. For an oversized file, check the size before reading the content.
Interpolate the observation time
sample(points,when,col=3) linearly interpolates that time in a validated waveform. col is an actual int 1, 2, or 3, when is a non-bool finite number, and both endpoints are allowed. A time outside the range and a wrong column are a ValueError.
Using the nearest next row as is differs from interpolating between two rows. Do not pad what is outside the range with the last value.
Distinguish input, switch, and stored voltage
observe(cfg,points) validates with config and then calculates the four observation times and target voltages. At observation, the absolute value of gate and the difference of vin from the target must each be at most 1µV. The maximum difference between the sample voltage and the voltage at the end of the hold must also be at most 1µV. It returns the observation dict below.
If you use v(vin) as the sample result, the residual error disappears. Do not confuse the 27ns in the time formula with the 1ns in the analytical formula.
Carry over the previous acquisition voltage
predict(cfg) validates the configuration and then returns a list of 4 voltages using a recursive formula that acquires the first channel twice and the second channel twice from an initial 0V. The connection time is acq_s+1ns and the resistance is rs_ohm+100Ω.
If you use the whole period as the charging time or reset to 0V every time, you end up calculating a different model.
Separate error, model, and boundary judgments
assess(cfg,observed) returns the LSB error, the maximum difference from the analytical formula, model agreement, the four sample judgments, and all_pass according to the judgment contract below. The samples_v and targets_v of the observation dict are 4 finite numbers, and if the target list differs from the configuration, it is a ValueError.
The 100µV model-check tolerance and the 10µV decision-margin band have different roles. Do not promote borderline to success.
Build a report from a real file
analyze(cfg,trace_path) connects config→read_trace→observe→assess and returns a dict with the four keys schema=1, config, observation, and assessment. File corruption is passed on, not turned into a success report.
Distinguish the target, predicted, and observed values, and preserve the original waveform and original configuration. Do not substitute a function that returns example numbers.
Connect the condition comparison to a real CLI
Implement campaign(configs,runner), main(argv), and the CLI entry point under the contract below. Validate all configurations first, analyze the 5ps and 2.5ps waveforms for each condition, and check convergence together with both judgments. It is checked with real ngspice and a separate CLI process.
A passing candidate is a candidate only within the stated model. Do not turn an execution failure into an empty success list, and do not overwrite a previous output folder either.