ADR-083 — Frequency-domain stimuli for circuit¶
Status¶
Accepted (2026-09-29, with the ac analysis of packages/qt-circuit).
Numbered ADR-040 until 2026-10-08, when it was renumbered because the
favourites record (ADR-040) landed first
under the same number (#342).
Extends ADR-019, which it does not replace: a transient stimulus is graded
exactly as before.
Context¶
ADR-019 grades a circuit answer by simulating it under the teacher's stimuli
and comparing the output WAVEFORM with the reference's, stimulus by stimulus.
A first-year filter question, though, is rarely about a waveform. "Build a
low-pass with its corner at 1 kHz" is a statement about a frequency response,
and the transient can only probe it one frequency at a time: a teacher who
wants the slope checked writes four sine stimuli, the maximum, and still
checks four points of a curve. The curve itself — the Bode plot — is what the
course draws on the board.
ngspice computes it directly. An .ac analysis linearises the circuit around
its DC operating point and solves it at log-spaced frequencies; the magnitude
and the phase of v(out) against a unit source are the Bode plot, in one run
of the same container, with no settling time to skip.
What had to be decided is how such a stimulus is authored, what is measured, and what "the same response" means, since the transient's rule — an RMS distance as a share of the swing — has no meaning on a curve in decibels spanning ten decades.
Decision¶
-
The analysis of a stimulus becomes a union.
{ kind: "tran", stopMs, skipMs, points }is the transient of ADR-019 and the default whenkindis absent: every config written before stays valid, the tag is filled in before the discriminated union reads it, and the canonical file omits it for a transient, so an export is byte for byte what it was.{ kind: "ac", fStartHz, fStopHz, pointsPerDecade }is the sweep: 0.01 Hz to 1 GHz, 5 to 200 points per decade (20 by default), at most 2000 points in all — the ceiling ADR-019 already set on one simulation. NoconfigVersionbump: nothing stored needs rewriting. -
The source of a sweep is its DC bias. An AC stimulus requires a
dcsource; itsvoltsis the operating point the circuit is linearised around, and the deck drives the EMF withDC <volts> AC 1. A sine, a pulse or a step has no single operating point and is refused at publication (circuit.ac_needs_dc_source). The editor shows one "bias" field instead of the source picker. -
What is measured is
v(out)against the source's EMF, the unit AC source behindsourceOhms: the transfer of the whole harness, the source resistance and the load included. Only its magnitude (dB) and phase (degrees) are plotted and compared; the input is the unit source and a flat line, and the load current of the transient plot has no AC counterpart. -
The pass rule is an envelope around the reference's Bode plot, set by three knobs in the grading block,
grading.bode = { magDb: 1, floorDb: 60, phaseDeg: 10 }, besidegrading.tolerance— which stays the transient's alone. They are in the grading block and never in the analysis because the analysis travels to the student (the Simulate button needs it) and the pass rule does not:toStudentstrips the grading block whole (invariant 4). -
The floor is RELATIVE: the reference's own peak minus
floorDb, so a unity-gain filter and a ×100 amplifier are judged alike. - Where the reference is at or above the floor, the student's magnitude
must be within
magDbof it, and — unlessphaseDegisnull— the phase difference, reduced modulo 360° to (−180°, 180°], withinphaseDeg. - Where the reference is below the floor, the bound is one-sided: the
student's output must stay under
floor + magDb. The reference says "nothing gets through" there, any output small enough says the same, and the phase of a signal 60 dB down is numerical noise, so it is not compared. - The stimulus passes when EVERY frequency passes, and its points are earned whole, as for a transient.
Both runs are built from the same .ac line and share one frequency grid,
so they are compared point for point with no resampling; two tables of
different grids are a failed stimulus (grid_mismatch), never a guess. The
rule runs on the FULL ngspice table: decimation to 250 points is for the
stored details and the plot, and a resonance peak it steps over is exactly
what the envelope must see. Both runs use ngspice's continuous phase
(cph), identically, so an inverting amplifier reads 180° across the band
instead of jumping between +180° and −180°. The table carries the linear
magnitude, converted to dB in the parser and floored at −300 dB, because
db() of an output that is exactly 0 V is an ngspice error that prints no
table at all.
- The details carry a Bode series and an envelope.
SeriesSetgains{ kind: "ac", f, magDb, phaseDeg }; a series withoutkindis a transient, so every stored grading still parses. An ACStimulusDetailhaserror: nullandenvelope: { worstDb, worstDeg, outside }, and the results page shows "1.8 dB · 12°" — never a dB figure dressed as a percentage of a swing. A hidden stimulus loses its curves for a student exactly as before and keeps its verdict and its envelope, which are verdicts likeerror; the reference's curve travels only undershowExpectedor a shown key.
Consequences¶
- A filter question checks the whole response in one stimulus: corner, slope and phase at once, over as many decades as the teacher wants.
- The Player and the Review draw a Bode plot — magnitude above, phase below, a
logarithmic frequency axis — through the same
Plotcomponent, which picks the chart by the series' kind. - The result is a SMALL-SIGNAL linearisation. A saturated op-amp or an unbiased diode gives a Bode plot that means nothing, and a sharp resonance built with non-standard values can put a correct answer's peak a few dB away from the reference's; the editor says both next to the switch. The transient remains the right tool for anything nonlinear.
- A sweep runs in the same container, under the same limits and the same 2000-point ceiling as a transient; the runner and its image are unchanged.
Rejected alternatives¶
- The transient's RMS rule on the dB curve. A share of the swing means nothing when the swing is 100 dB and the interesting part is the 3 dB around the corner; and in the stop band it would demand that a student match −140 dB to the decibel, which is numerical noise.
- An absolute floor (say −60 dB). An amplifier with 40 dB of gain and a passive divider would not be judged by the same rule; relative to the reference's peak, they are.
- Comparing the phase everywhere. Below the floor the phase of a vanishing output is whatever the solver's rounding makes it, and a correct answer would fail on it.
- Knobs inside the analysis. Per-stimulus envelopes would travel to the student with the analysis, and would be a second tolerance model beside the transient's single question-level one. One envelope per question, in the grading block, is the transient's model applied to the sweep.
- Dividing by
v(in)instead of takingv(out)against the EMF. It would remove the source resistance from the transfer, which a teacher who set one wants to see, and would divide by a node the student's circuit can short. - A fraction-of-points knob ("pass if 95 % of the frequencies are in the envelope"). It hides the one frequency that is wrong — usually the one the question is about. The rule is binary, like the transient's.