ADR-1295: Correct sureal's Gaussian density for unanimously rated stimuli in-process¶
- Status: Proposed
- Date: 2026-09-22
- Deciders: VMAFx maintainers
- Tags:
python,numerical-correctness,dependencies,testing
Context¶
ADR-1278 made every warning fatal in the classic Python harness. Three routine_test.TestTrainOnDataset cases then failed, two of them on
sureal.tools.stats.vectorized_gaussian evaluates 1 / sqrt(2*pi) / scale * exp(-(x - loc)**2 / (2 * scale**2)) with no guard on scale. MosModel._get_mos_and_stats calls it with the per-stimulus sample standard deviation, which is exactly zero whenever every observer rated a stimulus identically. python/test/resource/raw_dataset_sample.py contains two such stimuli by construction — the reference videos, rated [100] * 5 and [90] * 5 — so scales[0] and scales[2] are 0.0.
The zero is legitimate input. Unanimous ratings of a reference stimulus are an ordinary subjective-test outcome, and nothing upstream produced a degenerate array by mistake.
The warning is not cosmetic. The leading division yields inf, the exponent's 0 / 0 yields nan, and inf * nan is nan; the numpy.nansum in sureal's log-likelihood then drops those observations while still dividing by the full observation count. Measured on that fixture, sureal 0.9.0 reports loglikelihood = -1.6932609057879255, which is the sum over the 10 observations that survived divided by all 20 — the 10 unanimous ones vanished. AIC and BIC inherit the error. sureal 0.9.0 is the newest release on PyPI, so there is no upstream version to upgrade to, and the package exposes no hook through which a replacement density could be registered.
The third failure is unrelated to sureal and is a plain call-site defect: RegressorMixin._plot_one_content obtains its per-content trend line from tools.misc.linear_fit, a scipy.optimize.curve_fit wrapper that also estimates a parameter covariance. A content group in dataset_sample.py holds two stimuli and a line has two free parameters, so no residual degree of freedom is left, SciPy fills the covariance with infinities and raises OptimizeWarning. This is not a second instance of the ADR-1278 5PL defect: that one was structural parameter redundancy in a five-parameter model that had b1 as an additive intercept alongside b5; here the parameters are perfectly identifiable and only the covariance — which the plot never reads — is not.
Decision¶
We will ship a fork-local, zero-scale-safe vectorized_gaussian in vmaf.tools.stats and bind it over sureal's own at import of vmaf.core.train_test_model, the single fork module that already hard-imports sureal and that every subjective-modelling entry point in vmaf.routine pulls in before a model is fitted. Where scale is zero the density takes its Dirac-delta limit — unbounded at x == loc, zero elsewhere, nan for a missing rating — so a dataset containing a unanimously rated stimulus reports loglikelihood = +inf (AIC and BIC -inf), the honest statement that its maximum likelihood is unbounded, rather than a silently truncated finite number. Every finite non-zero scale keeps sureal's expression unchanged and is bit-identical to it. Separately, the scatter plot fits its trend lines with numpy.polynomial.Polynomial.fit and stops requesting a covariance it neither can have nor reads.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Rebind a corrected density into sureal from the harness (chosen) | Fixes the arithmetic at its cause; non-degenerate results bit-identical; no new dependency; one small function | Patches a third-party module at a distance; covers only code paths that import the fork's harness | — |
Upgrade sureal | Zero fork-local code | 0.9.0 is the newest release and still carries the defect | No release to upgrade to |
Vendor sureal's subjective models into compat/python-vmaf/ | Full control; no patching | Copies ~1500 lines of a maintained dependency into the rebase surface, for a one-line defect | Cost wildly out of proportion |
Subclass MosModel / DmosModel in the harness | Explicit, no monkey-patching | The failing tests pass sureal.subjective_model.MosModel in directly, so a subclass is never reached | Does not fix the reported cases |
Report nan for the degenerate scale (preserve today's values) | No number moves anywhere | Keeps the silent truncation: the statistic stays wrong, only quietly | Trades one defect for its symptom |
| Drop degenerate stimuli before fitting | Removes the zero | Discards real ratings and changes the MOS the harness returns — the assertions in routine_test.py read those | Changes groundtruth |
Keep curve_fit and pass absolute_sigma=True for the trend line | One-word change | Silently redefines what the returned covariance means to suppress the diagnostic | Suppression wearing a fix's clothes |
Consequences¶
- Positive:
loglikelihood,aicandbicstop being computed over a subset of the observations; the degeneracy is reported instead of hidden. No warning filter,catch_warningswrapper orfilterwarningsmark was added. - Negative: a third-party module is patched at import time. The comment at
compat/python-vmaf/core/train_test_model.pynames this ADR so the next reader finds the reasoning, and the binding must be revisited if sureal ever ships a fix or moves the symbol. - Neutral / follow-ups: nothing in-tree consumes
loglikelihood,aicorbic, so no assertion moves; the two trend-line fits agree with the previouscurve_fitresult to 7.8e-14 in slope and intercept on the four-point overall fit. Fixing theOptimizeWarningalso unmasked a latentValueErrorin_plot_one_content: the HISS-04 helper extraction leftif point_labels:applied to the NumPy slice of the caller's label list instead of to the list itself, which is the "extraction, not redesign" invariant incompat/python-vmaf/AGENTS.md; the predicate is nowis not None.
References¶
- ADR-1278 — fatal warnings and the 5PL correction this is explicitly not a repeat of.
- ADR-0165 — the
docs/state.mdrow accompanying this change. - NumPy
nansum— treatsnanas zero, which is what hid the truncation. - SciPy
curve_fit—pcovis filled withinfwhen the residual degrees of freedom are zero. - NumPy
polynomial.Polynomial.fit— least-squares fit without a covariance estimate.