Interpreting results¶
The line for each target¶
As each capture finishes, Measure flicker prints one line for it:
| Part | Means | Good |
|---|---|---|
nominal |
The frequency the design asks for | — |
measured |
The frequency the screen produced | — |
Δ |
measured − nominal | Within the tolerance (±0.1 Hz by default) |
drift |
The largest timing wander during the capture | A fraction of a millisecond at 30–50 Hz. It is a phase measurement expressed as time, so the same wobble reads ten times larger at 4 Hz than at 40 Hz |
adc |
The lowest and highest sensor readings (0–1023) | Clear of both 0 and 1023, with a swing of at least a few dozen counts |
| verdict | PASS or FAIL, with the reasons after <- |
PASS, without [flagged] |
A failed target names why: |Δ|>0.1 (wrong frequency), clipped at 0 or clipped at 1023 (the reading hit a limit), desync (N lost) (bytes lost on the USB link), or a refusal name (below). A pass with an advisory flag reads PASS [flagged], and the flag is explained on the next line.
The run ends with a summary such as 9/9 targets passed; report → qc-reports/…json. Every target passing is the go-ahead. The command line exits with status 0 then, or for a visual-only run, and 1 otherwise.
When a target fails¶
| What you see | Likely meaning | What to do |
|---|---|---|
clipped at 0, Δ tiny, on every target |
The sensor is sealed light-tight, so the dark phase floors at 0 | Let a little room light in (Calibration); don't add gain or lower the brightness |
clipped at 1023 |
Too much light or gain, or a wiring fault | Lower the brightness; check the build |
desync |
Bytes were lost between the sensor and the PC | Distrust that capture; check the cable and re-run |
| Δ too large, capture otherwise clean | The display really is producing a different frequency | Check the refresh rate (Measure refresh); a frequency with few frames per cycle may be rendered only approximately |
| A refusal | The capture can't support any verdict | See Refusals |
On a high-refresh panel, a FAIL usually means one of two things: a frequency the display cannot render cleanly at that refresh rate (too few frames per cycle), or a measurement problem (clipping or lost bytes). The other fields tell them apart.
The report file¶
A JSON report is written for every run, including a visual-only one, to qc-reports/flikometer_<design>_<fingerprint>_<time>.json. The name ties it to the exact design (its name and manifest fingerprint) and the time of the run, so it cannot be mistaken for another design's. It holds monitor light measurements only, no participant data, and the qc-reports/ folder is not committed to git.
At the top level, all_passed is the overall verdict, notes record the timebase calibration and the display that was measured (screen, refresh rate and graphics renderer), and refusal_thresholds and flag_thresholds record the values this run was judged against. Each target has an entry in measurements:
| Field | Meaning, and what to watch for |
|---|---|
nominal_hz |
The frequency the design asked for |
measured_hz |
The measured frequency, corrected by the timebase calibration. The number to trust |
delta_hz |
measured_hz − nominal_hz. Must be within tolerance_hz to pass |
slope_freq_hz |
A second, independent estimate of the frequency, from the phase. Should agree with measured_hz. null means it could not be measured in this capture: too short for slow flicker (below 20 Hz it needs about 19 cycles), or no signal |
max_abs_drift_ms |
The largest timing wander during the capture; large values mean unstable flicker. null for the same reasons as slope_freq_hz. A single dropped frame shows up here but does not fail the target |
clipped |
The reading hit 0 or 1023. adc_min and adc_max say which, and the fixes are opposite |
adc_min, adc_max |
The lowest and highest readings (0–1023). adc_max at 1023: too much light or gain. adc_min at 0: the dark phase bottomed out; let a little room light in |
counters_ok, dropped_samples |
Whether the data stream arrived intact. false or a non-zero count: distrust the capture |
rel_amplitude |
How much of the capture is the flicker (the flicker peak's share of the whole spectrum). Low means the sensor saw little of it. It also falls as the capture gets longer, so compare it only between captures of similar length |
n_samples |
Capture length in samples (divide by fs_effective_hz for seconds) |
fs_effective_hz |
The calibrated sample rate the analysis used |
passed |
Δ within tolerance, and the stream intact, and not clipped, and no refusals. Flags play no part |
refusals |
Named reasons the capture cannot support a pass; empty when it can |
flags |
Advice beside the verdict, never part of it; empty when there is none |
Refusals¶
A sensor sitting beside the target still gives the analysis a spectrum with a highest peak in it, and if that peak happened to land within the tolerance the target used to pass, on a capture with essentially no flicker in it. That is worse than a fail, because it creates a record that someone checked. So a capture that cannot support a pass is refused: the target fails, and the reason is named on its line (FAIL <- flat_trace, weak_signal), in a refused … line under it with a hint, and in the report's refusals.
| Refusal | Means | What to do |
|---|---|---|
flat_trace |
The flicker's own amplitude is under about 16 counts. This is the size of the flicker itself, not the raw swing, which drifting room light and noise inflate | The sensor is off the target, shaded, or the gain is far too low. Re-seat it and re-run |
weak_signal |
The flicker is a tiny share of the capture (rel_amplitude under 0.02) |
Re-seat the sensor and check nothing shades it. If the sensor was fine, the display may be rendering this frequency poorly |
too_few_cycles |
Fewer than 5 cycles of the flicker in the capture | Lengthen the capture (--stim-s). The dialog lengthens it for the slowest target by itself, so a refusal there means something else is wrong |
ambiguous_peak |
A second peak near the expected frequency is more than half as strong as the main one | The capture is noise or holds more than one frequency; the estimate can't be trusted |
off_target_dominant |
A peak well away from the expected frequency is more than 3 times the target's | Flickering room lights or the wrong stimulus dominate the capture. A weaker stray peak, such as the alias of a coarsely rendered sine wave, is not refused |
frequency_changed |
The flicker drifted by more than a quarter cycle from a steady frequency, or its two frequency estimates disagree by more than 5 % | The display did not hold one frequency through the capture; re-run it |
frequency_changed is judged only when drift can be measured. It cannot see a change in the first or last stretch of the capture (the filter discards those), a change of about 0.5 Hz or less late in it, a single dropped frame, or anything in a capture too short for drift to be measured.
Flags¶
Flags are advice. They never change the verdict, but a pass that carries one is marked PASS [flagged] and worth reading before you rely on it.
| Flag | Means | What to do |
|---|---|---|
slope_disagrees |
The two frequency estimates differ by more than 1 % | The flicker was not one steady frequency, or the capture is not the flicker. Re-run it and look at the drift |
slope_unmeasurable |
There is no second estimate to confirm the first | Expected for a short capture of slow flicker (below 20 Hz the second estimate needs about 19 cycles; --stim-s lengthens the capture). On any other target it means there was no signal |
The thresholds behind refusals and flags are deliberately loose, set to refuse only plainly worthless captures; the technical reference has the details and their limits.
Dark reference figures¶
A run with a dark reference adds two things to the report. None of it affects passed or all_passed.
dark_reference describes the black-screen capture: where it was taken (position), its adc_mean, adc_sd, adc_min and adc_max, n_samples, and usable, which is false if the stream fell out of step during it, in which case nothing is computed against it.
dark_relative has an entry for each target:
| Field | Meaning |
|---|---|
level_low, level_high |
The capture's two plateaus, dark and bright. Contrast is computed from these, not from adc_min and adc_max, which noise pushes outward |
modulation_depth_raw |
(high − low) / (high + low) from the raw plateaus: too low, because of the room-light offset |
modulation_depth |
The same with the dark reference subtracted from both plateaus. In sensor units: good for comparing one pattern, frequency or build with another, not a perceived contrast. A value near 1 does not by itself mean the sensor sat fully on one check; swing_over_dark_range and snr_vs_dark_db say how much light there was. It is capped at 1 when noise would push it over; modulation_depth_clipped_from keeps the uncapped value. Both depths are null when the capture is indistinguishable from dark or the corrected values are within noise of zero, because there is no contrast to compute |
trough_above_dark |
How far the dark plateau sits above the dark reference, in counts. A reversing check that never gets back to dark is a contrast defect, and this is where it shows |
swing_over_dark_range |
The capture's swing divided by the dark capture's |
snr_vs_dark_db |
The strongest peak near the target frequency, compared with the dark capture's strongest in the same band. Near 0 dB for a capture that is only noise |
flags |
indistinguishable_from_dark: the swing is no more than twice the dark capture's, so there is nothing in it the dark capture didn't have. trough_below_dark: the dark plateau is well below the dark reference, so the reference was taken somewhere else or the light changed, and modulation_depth should not be trusted |
Marker-to-light latency¶
scripts/extract_light_latency.py writes qc-reports/light-latency_<time>.json (settings, inputs, summaries and every trial) and a per-trial .tsv. For each trial it gives two estimates of the delay:
- Edge latency: when the light first crosses halfway from its dark level to its lit level.
- Phase lag: the delay implied by the flicker's phase at the marker, compared with the phase the stimulus should have at onset. It is much less sensitive to the amplifier's sampling rate than the edge.
It also gives the onset phase in degrees. A trial with no clear dark-to-light step, or one already lit before onset, is listed with its reason and gets no number.
The summaries come at two levels. Per segment (one continuous stretch of one recording): mean, standard deviation and drift per minute. Per light source, amplifier and frequency: three kinds of spread kept apart, namely the jitter within a segment, the drift across trials, and how much the segment means differ (what changes when the acquisition restarts). A constant delay reads the same in milliseconds at 10 Hz and 40 Hz.
What is in the number. The marker is sent as the first stimulus frame is drawn, so every latency includes the wait for that frame to be displayed, the time for the panel to scan down to the target and the pixels to respond, and the amplifier's own filter delay. The display's share is the same whichever amplifier you use, so the difference between two amplifiers on the same display belongs to the acquisition path.
The method's own floor. At a 250 Hz amplifier rate the edge estimate is uncertain by a few tenths of a millisecond per trial, so a spread at that level is the method, not the amplifier.
More on segments, timestamps and channel labels is in the technical reference.