TT Lab
Get started
Learn Learning paths Courses

Electronics Foundations — Validating Sensor Inputs

A Greenhouse Fan Vibrates Under Another Name

Continue in TT Lab

Goal

Reproduce the situation in which the greenhouse fan's 875Hz vibration looks like 125Hz, and report the range that can be distinguished by changing the analog filter and the observation rate.

Why it matters

Proceed after knowing the earlier RC and ADC acquisition-time units, Python functions, lists, dicts, exceptions, file handling, and complex numbers and trigonometric functions. The lab takes an expected 120 minutes. Extend with +time before the default 60-minute session ends. The maximum is 180 minutes, and files disappear when it ends. Keep the code and report separately. No real equipment is connected and no voltage is applied.

Environment and deliverables

The file you write is just /root/aliasing/analyze.py. Python3 and ngspice42 are in the image, and no external downloads or added permissions are needed. Add the folder of /opt/lab/fixtures/aliasing/aliaswave.py to sys.path and import it. This helper's capture(cfg,new_folder) runs the real circuit and returns the path new_folder/trace.tsv. It also leaves input.cir, config.json, and solver.log. The parent folder must already exist, and an existing folder is not overwritten.

cfg is an exact dict of 3 keys. input_hz is one of 125, 375, 625, and 875, cutoff_hz is one of None, 300, and 80, and dt_s is one of 2e-6 and 1e-6. Numeric bool, strings, NaN, and infinity are rejected. The circuit is a 1V cosine input, a 1kΩ series resistor, and a ground capacitor with C=1/(2π1000fc). If it is None, a 10¹²Ω load replaces C. It calculates the full 90ms and records the input and output together. The acquisition switch model of the earlier unit is not in this circuit.

The provided load_trace(path) validates a waveform of at most 16MiB and returns a list of tuples with three columns: time, input, and output. The header is time v(in) v(out), and there are 2 to 200000 data rows. Time must be 0 or greater and strictly increasing, the first time ≤1µs, and the last time within 1e-12 seconds of 90ms. Every value is finite and the absolute voltage is ≤1e6V. Corruption is a ValueError, and OS file errors are passed through as they are. resample accepts rows that passed this validation, so you do not need to repeat the file check on every call.

Function numeric contract

The stated numbers allow only int or float, and bool, strings, NaN, infinity, and exceeding the float conversion range are ValueErrors. Do not modify the original input or the original file. The keys of the returned dict are as below, and do not add unnecessary fields.

Function Input range and return
alias_hz 0≤freq≤1e9, 0
resample 100≤fs≤8000, start≥0, count is an actual int 2 to 256; a list of output voltages at times start+i/fs
spectrum values is an exact list, even length 2 to 256, each absolute voltage ≤1e6; 100≤fs≤8000
rc_gain 0≤freq≤1e9, 1 if cutoff=None, otherwise 1e-6≤cutoff≤1e9
rc_window 1e-6≤wanted

Each entry of spectrum is bin (0 to N/2), hz=k*fs/N, and amplitude_v. Z=(1/N)Σvalues[i]exp(−2jπki/N), and the amplitude is abs(Z) if k=0 or N/2, and 2abs(Z) otherwise. For tied peaks, choose the first bin.

analyze accepts only fs=1000 or 4000. It analyzes with a default start of 0.024 seconds and 64 samples, and the returned keys are fs_hz, start_s, n, samples_v, spectrum, peak_hz, and peak_amplitude_v. peak_hz is the bin frequency of the maximum amplitude, but it is None if that amplitude ≤1e-6V. peak_amplitude_v keeps the real maximum even in that case. Do not turn a file-reading error into an arbitrary no-signal result.

compare_reports accepts normal analyze reports. If any one of fs_hz, start_s, and n differs, or the length of samples_v differs from n or is empty, it is a ValueError. The returned max_sample_difference_v is the maximum absolute difference between samples, and indistinguishable is a bool for whether that value ≤1e-4V. It does not compare only the amplitude spectrum.

rc_gain is 1/sqrt(1+(freq/cutoff)2). The returned values of rc_window are min_cutoff_hz=wanted/sqrt(1/min_gain2−1), max_cutoff_hz=nuisance/sqrt(1/max_gain**2−1), and feasible=(lower bound≤upper bound). feasible=False is also a normal result, and do not change the requirement to make it True.

assess_filter accepts two normal analyze reports and checks that each one's peak_amplitude_v is a finite number of 0 or more. The returned keys are wanted_amplitude_v, nuisance_amplitude_v, passband_ok (first amplitude ≥.9), stopband_ok (second ≤.1), and all_pass (AND of the two conditions). This judgment is the contract of this experiment, which has a 1V input and integer-period observation.

Overall report and CLI contract

The inputs of campaign(inputs,runner) is an exact list and accepts only [125,875] or [375,625]. The elements follow the numeric rules above. By order, the first is the desired input and the second is the interference. Validate all the inputs first, and then call runner(cfg). runner returns a waveform path, and exceptions are passed to the caller. The original input is preserved.

The execution order is cutoff None→300→80, then the order of the input pair within each cutoff, then dt 2e-6→1e-6 for each input. That is 12 runs in total, and for each waveform you run analyze(path,1000) and analyze(path,4000). The trials entries for each of the 6 conditions are as follows.

filter_results has 3 entries in cutoff order. The keys of each entry are cutoff_hz, assessment (assess_filter of coarse.low of the two inputs), fine_assessment (assess_filter of fine.low of the two inputs), valid (both inputs converged and the model agrees), and eligible (valid, and the all_pass of both assessments are also True).

The top-level returned keys are schema=1, trials, filter_results, single_rc_window=rc_window(*inputs), collision (compare_reports of the coarse.low of the two no-filter inputs), and scope='ideal-rc-and-uniform-sampling-only'. If there is no single-RC candidate, leave eligible as False as it is. Do not hide an error with an empty trials.

main(argv) takes two arguments: an input-pair JSON file and the path of an output folder that does not exist yet. Validate the JSON and the input pair first, and then create the output folder. The parent must already exist. In the order of runner calls, call the provided aliaswave.capture into new folders run-00..run-11 under it. 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 already made even on failure. On import, do not run, and exit with main's return value only when run directly.

The provided input example is /opt/lab/fixtures/aliasing/inputs.json. To run it directly in the final step, choose a new output name. Be careful that a report redirect does not overwrite existing material either.

python3 /root/aliasing/analyze.py /opt/lab/fixtures/aliasing/inputs.json /root/aliasing/trial-01 > /root/aliasing/report-01.json

Steps

  1. The other name a fast signal gets: write alias_hz(freq,fs) in /root/aliasing/analyze.py. It returns the folded frequency using f mod fs and the symmetry of the first Nyquist zone. It handles multiple zones, DC, and the boundary, and follows the numeric contract below.
  2. Use the observation time instead of the row number: add resample(rows,fs,start=.024,count=64) to the same file. It returns a list of the output voltage at start+i/fs, linearly interpolated from the validated waveform. The endpoints are included, and an observation outside the range is rejected with a ValueError.
  3. Tell apart the bins whose amplitude doubles: add spectrum(values,fs). From a DFT normalized by 1/N, build a list of dicts containing bin, hz, and amplitude_v for k=0..N/2. Only interior bins are doubled, and DC and Nyquist are 1 times.
  4. Read the same waveform again at two observation rates: add analyze(path,fs). Read the file with the provided load_trace, connect the observation and the DFT, and return the report contract below. fs is 1000 or 4000, and the real sample times must change too.
  5. Distinguish the same spectrum from the same samples: add compare_reports(a,b). After checking the 3 observation-clock fields and the sample count, return the maximum sample difference and whether it is within 100µV. A different clock or empty samples is a ValueError.
  6. Calculate an impossible filter requirement: add rc_gain(freq,cutoff) and rc_window(wanted,nuisance,min_gain=.9,max_gain=.1). Calculate the gain at the original input frequency, and return the allowed fc interval for the two band requirements and whether the intersection exists.
  7. Ask whether it also protects the desired signal: add assess_filter(wanted,nuisance). From the maximum amplitudes of the two analyze reports, judge desired signal ≥0.9V and interference ≤0.1V respectively, and return the AND of the two conditions.
  8. Bundle circuit, clock, and convergence as evidence: add campaign(inputs,runner), main(argv), and the CLI entry point. Run the 12 circuits in the call order below and connect the 24 analyses. Distinguish convergence, model, and requirement, and preserve the real waveforms and the JSON report.

Notes

Grading checks the step-by-step functions and the preservation of the original material. At the end it also runs real ngspice and a separate CLI in a temporary folder and compares the stored waveforms and reports. It cleans up only the check-only folder and does not change the student files. The helper's input constraints are to make the experimental range clear and are not a model of every ADC and filter. The noise, jitter, quantization, and reliability across temperature of a real board are not verified.

The other name a fast signal gets

Write alias_hz(freq,fs) in /root/aliasing/analyze.py. It returns the folded frequency using f mod fs and the symmetry of the first Nyquist zone. It handles multiple zones, DC, and the boundary, and follows the numeric contract below.

Subtracting the observation rate once is different from taking the remainder. A bool behaves like an integer in Python, but do not accept it as a configuration number.

Use the observation time instead of the row number

Add resample(rows,fs,start=.024,count=64) to the same file. It returns a list of the output voltage at start+i/fs, linearly interpolated from the validated waveform. The endpoints are included, and an observation outside the range is rejected with a ValueError.

The columns of rows are in the order time, input, output. Think about the case of an exactly matching row and the case between two rows separately.

Tell apart the bins whose amplitude doubles

Add spectrum(values,fs). From a DFT normalized by 1/N, build a list of dicts containing bin, hz, and amplitude_v for k=0..N/2. Only interior bins are doubled, and DC and Nyquist are 1 times.

You can check the endpoint scaling with a constant voltage and with samples that alternate in sign. This value is an amplitude, not a power density.

Read the same waveform again at two observation rates

Add analyze(path,fs). Read the file with the provided load_trace, connect the observation and the DFT, and return the report contract below. fs is 1000 or 4000, and the real sample times must change too.

The peak_hz of the analysis result is the observed name. Do not express it as having found the original input frequency, and do not create a frequency for a no-signal case.

Distinguish the same spectrum from the same samples

Add compare_reports(a,b). After checking the 3 observation-clock fields and the sample count, return the maximum sample difference and whether it is within 100µV. A different clock or empty samples is a ValueError.

Even with the same magnitude spectrum, the phase can differ. You must compare voltages at the same time points.

Calculate an impossible filter requirement

Add rc_gain(freq,cutoff) and rc_window(wanted,nuisance,min_gain=.9,max_gain=.1). Calculate the gain at the original input frequency, and return the allowed fc interval for the two band requirements and whether the intersection exists.

The RC filter comes before sampling. Do not compute the gain with the folded frequency, and preserve as they are the results in which the lower bound exceeds the upper bound.

Ask whether it also protects the desired signal

Add assess_filter(wanted,nuisance). From the maximum amplitudes of the two analyze reports, judge desired signal ≥0.9V and interference ≤0.1V respectively, and return the AND of the two conditions.

Even if you reduced the interference, if you lose the desired signal too, you do not satisfy the design requirement. Keep the observed values and the judgment fields together.

Bundle circuit, clock, and convergence as evidence

Add campaign(inputs,runner), main(argv), and the CLI entry point. Run the 12 circuits in the call order below and connect the 24 analyses. Distinguish convergence, model, and requirement, and preserve the real waveforms and the JSON report.

An empty candidate list can be a normal design conclusion. Do not turn an execution exception into an empty success list, and do not overwrite an existing evidence folder.