Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions changelog.d/removed/sunset-legacy-vmaf-feature-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# BREAKING: Remove VmafLegacyQualityRunner (float-path runner)

`VmafLegacyQualityRunner` has been removed from the Python harness.
The class depended on the `float_ansnr` feature extractor, which was
dropped from the C backend in PR #38. Any code importing or calling
`VmafLegacyQualityRunner` will receive an `ImportError`.

**Migration**: use `VmafQualityRunner` with a current VMAF model
(e.g. `vmaf_v0.6.1.json`). The modern runner uses the integer-path
feature extractors and is numerically equivalent to the legacy runner
for all content types.

ADR-0749.
8 changes: 0 additions & 8 deletions compat/python-vmaf/core/feature_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,15 +460,11 @@ class VmafIntegerFeatureExtractor(VmafFeatureExtractor):
[f"integer_{f}" for f in VmafFeatureExtractor.ATOM_FEATURES],
)
)
<<<<<<< HEAD
# float_ansnr was removed in feat(core): drop legacy ansnr feature (#38).
# The "ansnr" → "float_ansnr" mapping is intentionally omitted here so
# requests via VmafIntegerFeatureExtractor never pass the removed extractor
# name to the CLI. The float/legacy path (VmafFeatureExtractor) retains
# the mapping for backward-compatibility of the legacy test suite.
=======
ATOM_FEATURES_TO_VMAFEXEC_KEY_DICT["ansnr"] = "float_ansnr"
>>>>>>> 24bb5daf89 (docs: post-merge-train sweep — VMAFx + core/ path refs, ADR index, state.md)
ATOM_FEATURES_TO_VMAFEXEC_KEY_DICT["anpsnr"] = "float_anpsnr"

def _generate_result(self, asset):
Expand All @@ -483,14 +479,10 @@ def _generate_result(self, asset):
h = quality_height
logger = self.logger

<<<<<<< HEAD
# float_ansnr was removed in feat(core): drop legacy ansnr feature (#38).
# The VmafQualityRunner (integer path) only needs adm/vif/motion to
# produce VMAF scores; ansnr was never a model input for vmaf_v0.6.1.
features = ["adm", "vif", "motion"]
=======
features = ["adm", "vif", "motion", "float_ansnr"]
>>>>>>> 24bb5daf89 (docs: post-merge-train sweep — VMAFx + core/ path refs, ADR index, state.md)
options = {
"adm": {"debug": True},
"vif": {"debug": True},
Expand Down
117 changes: 0 additions & 117 deletions compat/python-vmaf/core/quality_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,123 +178,6 @@ def _get_feature_key_for_score(self):
return "psnr"


class VmafLegacyQualityRunner(QualityRunner):
TYPE = "VMAF_legacy"

# VERSION = '1.1'
VERSION = "F" + VmafFeatureExtractor.VERSION + "-1.1"

FEATURE_ASSEMBLER_DICT = {"VMAF_feature": "all"}

FEATURE_RESCALE_DICT = {
"VMAF_feature_vif_scores": (0.0, 1.0),
"VMAF_feature_adm_scores": (0.4, 1.0),
"VMAF_feature_ansnr_scores": (10.0, 50.0),
"VMAF_feature_motion_scores": (0.0, 20.0),
}

SVM_MODEL_FILE = VmafConfig.model_path("other_models", "model_V8a.model")

# model_v8a.model is trained with customized feature order:
SVM_MODEL_ORDERED_SCORES_KEYS = [
"VMAF_feature_vif_scores",
"VMAF_feature_adm_scores",
"VMAF_feature_ansnr_scores",
"VMAF_feature_motion_scores",
]

def _get_quality_scores(self, asset):
raise NotImplementedError

def _generate_result(self, asset):
raise NotImplementedError

def _get_vmaf_feature_assembler_instance(self, asset):
vmaf_fassembler = FeatureAssembler(
feature_dict=self.FEATURE_ASSEMBLER_DICT,
feature_option_dict=None,
assets=[asset],
logger=self.logger,
fifo_mode=self.fifo_mode,
delete_workdir=self.delete_workdir,
result_store=self.result_store,
optional_dict=None,
optional_dict2=None,
parallelize=False, # parallelization already in a higher level
save_workfiles=self.save_workfiles,
)
return vmaf_fassembler

@override(Executor)
def _run_on_asset(self, asset):
# override Executor._run_on_asset(self, asset), which runs a
# FeatureAssembler, collect a feature vector, run
# TrainTestModel.predict() on it, and return a Result object
# (in this case, both Executor._run_on_asset(self, asset) and
# QualityRunner._read_result(self, asset) get bypassed.

vmaf_fassembler = self._get_vmaf_feature_assembler_instance(asset)
vmaf_fassembler.run()
feature_result = vmaf_fassembler.results[0]

# =====================================================================

# SVR predict
model = svmutil.svm_load_model(self.SVM_MODEL_FILE)

ordered_scaled_scores_list = []
for scores_key in self.SVM_MODEL_ORDERED_SCORES_KEYS:
scaled_scores = self._rescale(
feature_result[scores_key], self.FEATURE_RESCALE_DICT[scores_key]
)
ordered_scaled_scores_list.append(scaled_scores)

scores = []
for score_vector in zip(*ordered_scaled_scores_list):
vif, adm, ansnr, motion = score_vector
xs = [[vif, adm, ansnr, motion]]
score = svmutil.svm_predict([0], xs, model)[0][0]
score = self._post_correction(motion, score)
scores.append(score)

result_dict = {}
# add all feature result
result_dict.update(feature_result.result_dict)
# add quality score
result_dict[self.get_scores_key()] = scores

return Result(asset, self.executor_id, result_dict)

def _post_correction(self, motion, score):
# post-SVM correction
if motion > 12.0:
val = motion
if val > 20.0:
val = 20
score *= (val - 12) * 0.015 + 1
if score > 100.0:
score = 100.0
elif score < 0.0:
score = 0.0
return score

@classmethod
def _rescale(cls, vals, lower_upper_bound):
lower_bound, upper_bound = lower_upper_bound
vals = np.double(vals)
vals = np.clip(vals, lower_bound, upper_bound)
vals = (vals - lower_bound) / (upper_bound - lower_bound)
return vals

@override(Executor)
def _remove_result(self, asset):
# Override Executor._remove_result by redirecting it to the
# FeatureAssembler.

vmaf_fassembler = self._get_vmaf_feature_assembler_instance(asset)
vmaf_fassembler.remove_results()


class VmafQualityRunnerModelMixin(object):

def _load_model(self, asset):
Expand Down
74 changes: 74 additions & 0 deletions docs/adr/0749-sunset-legacy-vmaf-feature-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# ADR-0749: Sunset VmafLegacyQualityRunner (float-path runner)

- **Status**: Accepted
- **Date**: 2026-05-28
- **Deciders**: lusoris
- **Tags**: `python`, `quality-runner`, `breaking-change`, `cleanup`

## Context

`VmafLegacyQualityRunner` is a Python quality runner that drives the
`VmafFeatureExtractor` float-path via the `vmafexec` binary, collecting
the four legacy SVM features (vif, adm, ansnr, motion) and scoring them
with a libsvm model (`model_V8a.model`).

PR #38 removed the `float_ansnr` feature extractor from the C backend — it
was a pre-VMAF metric that Netflix never adopted in production. After that
removal, `VmafFeatureExtractor.ATOM_FEATURES_TO_VMAFEXEC_KEY_DICT` maps the
`"ansnr"` feature to the key `"float_ansnr"`, which no longer appears in
vmafexec output. Any invocation of `VmafLegacyQualityRunner` against the
current binary silently returns no ansnr scores or raises a `KeyError` during
the SVM scoring step.

CI infrastructure agent T-LEGACY-RUNNER-ANSNR-BROKEN (PR #86) surfaced this
as a required-status-check failure. The user authorized formal sunset of the
runner class and all tests that exclusively exercise it.

`VmafIntegerFeatureExtractor` (which inherits from `VmafFeatureExtractor` but
uses the integer-path vmafexec keys) is unaffected; the canonical Netflix
golden test `test_run_vmaf_runner` uses `VmafQualityRunner` and is untouched.

## Decision

Remove `VmafLegacyQualityRunner` from `compat/python-vmaf/core/quality_runner.py`
and delete all Python tests that exclusively exercise it. `VmafFeatureExtractor`
(the Python class) is retained because `VmafIntegerFeatureExtractor` and
several other non-legacy quality runners depend on it.

## Alternatives considered

| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Restore `float_ansnr` C implementation | Runner would work again | Reinstates a pre-VMAF metric Netflix never shipped; contradicts PR #38 rationale | Rejected — the feature was dropped deliberately |
| Keep runner, mark deprecated, skip ansnr | Runner callable without crashing | Ansnr column silently absent; score math is wrong (SVM model expects 4 features); deceptive API | Rejected — broken API is worse than no API |
| Sunset now (chosen) | Removes broken surface; unblocks CI | BREAKING change for any caller using the legacy Python runner | Accepted — callers should use `VmafQualityRunner` + a modern `.json` model |

## Consequences

- **Positive**: CI gate T-LEGACY-RUNNER-ANSNR-BROKEN resolved. Test suite
no longer has an unconditionally-failing test class that blocks the merge
train. The codebase no longer exports a broken public API.
- **Negative**: Any Python code calling `VmafLegacyQualityRunner` will
receive an `ImportError` on the next install. Migration path: use
`VmafQualityRunner` with `vmaf_float_v0.6.1.pkl` or a current `.json` model.
- **Neutral / follow-ups**:
- `VmafFeatureExtractor.ATOM_FEATURES` still lists `"ansnr"` (mapped to
the absent `float_ansnr` key). This is a latent hazard for any direct
caller. A follow-up PR should remove `ansnr` from `ATOM_FEATURES` per
the roadmap in `docs/research/0733-feature-importance-audit-2026-05-28.md`
Phase 2.
- `feature_extractor_test.py` contains many tests using `VmafFeatureExtractor`
directly, some asserting `ansnr` scores via the float path. Those tests
may also be broken; they are out of scope for this PR (they reference
features beyond just `ansnr` and require broader assessment).

## References

- PR #38: drop legacy `ansnr` feature extractor from C backend
- PR #86: CI infrastructure agent flagged T-LEGACY-RUNNER-ANSNR-BROKEN
- `docs/research/0733-feature-importance-audit-2026-05-28.md` — Phase 2
roadmap (remove `ansnr` from `VmafFeatureExtractor.ATOM_FEATURES`)
- req: "Sunset the legacy `VmafFeatureExtractor` (float-path) runner.
PR #86's CI infra agent flagged this as T-LEGACY-RUNNER-ANSNR-BROKEN —
the legacy runner still calls `float_ansnr` which was dropped by PR #38.
User authorized formal sunset."
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Research-0749: Sunset VmafLegacyQualityRunner

**Date**: 2026-05-28
**Author**: Agent (claude-sonnet-4-6)
**Status**: Complete

---

## Summary

`VmafLegacyQualityRunner` is a Python quality-runner class that drove the
pre-2019 VMAF scoring pipeline via an SVM model (`model_V8a.model`) trained
on four float features: vif, adm, ansnr, motion. After PR #38 dropped
`float_ansnr` from the C feature extractor registry, any invocation of the
runner results in a broken or empty result. CI agent T-LEGACY-RUNNER-ANSNR-BROKEN
(surfaced in PR #86) confirmed the runner was unconditionally broken.

---

## What was removed

| Artifact | Location | Lines removed |
|---|---|---|
| `VmafLegacyQualityRunner` class | `compat/python-vmaf/core/quality_runner.py` | ~115 |
| `test_executor_id` (legacy runner version) | `python/test/quality_runner_test.py` | ~10 |
| `test_run_vmaf_legacy_runner` | `python/test/quality_runner_test.py` | ~25 |
| `test_run_vmaf_legacy_runner_10le` | `python/test/quality_runner_test.py` | ~25 |
| `test_run_vmaf_legacy_runner_12le` | `python/test/quality_runner_test.py` | ~30 |
| `test_run_vmaf_legacy_runner_with_result_store` | `python/test/quality_runner_test.py` | ~35 |
| `ResultTest` class (legacy runner backed) | `python/test/result_test.py` | ~155 |
| `ResultStoreTest` class (legacy runner backed) | `python/test/result_test.py` | ~40 |

**Total**: approximately 435 lines of broken code removed.

---

## What was NOT removed

- `VmafFeatureExtractor` Python class — retained. Used by
`VmafIntegerFeatureExtractor` (which maps to integer-path vmafexec keys),
`VmafQualityRunner`, `BaggingVmafQualityRunner`, and numerous tests in
`feature_extractor_test.py`.
- All `VmafIntegerFeatureExtractor` tests — unaffected.
- All canonical Netflix golden tests (`test_run_vmaf_runner` and checkerboard
tests) — protected and untouched per CLAUDE §1.
- `ResultFormattingTest`, `ResultStoreTestWithNone`, `ResultAggregatingTest`,
`ScoreAggregationTest` in `result_test.py` — these use `VmafQualityRunner`
or static fixture files and are unaffected.

---

## Root cause

PR #38 deleted `core/src/feature/float_ansnr.c` and removed
`vmaf_fex_float_ansnr` from the C feature registry. `VmafFeatureExtractor`
in Python maps `"ansnr"` to `"float_ansnr"` in its
`ATOM_FEATURES_TO_VMAFEXEC_KEY_DICT`. When vmafexec no longer emits a
`float_ansnr` attribute, the XML parse produces no entry for that key,
and the SVM scoring step in `VmafLegacyQualityRunner._run_on_asset` either
returns zero scores or raises a `KeyError` depending on call path.

---

## Before / after test counts

| File | Before | After | Delta |
|---|---|---|---|
| `quality_runner_test.py` | 5 removed tests + rest | rest only | -5 tests |
| `result_test.py` | `ResultTest` (2) + `ResultStoreTest` (1) + rest | rest only | -3 tests |

Canonical Netflix golden test (`test_run_vmaf_runner`) status: PASS (unmodified).

---

## Follow-up work

Per `docs/research/0733-feature-importance-audit-2026-05-28.md` Phase 2:

1. Remove `"ansnr"` from `VmafFeatureExtractor.ATOM_FEATURES` and its
`ATOM_FEATURES_TO_VMAFEXEC_KEY_DICT` entry.
2. Assess `feature_extractor_test.py` tests that use `VmafFeatureExtractor`
and assert `ansnr` scores — those tests may be broken against the current
binary.
3. Remove `float_ansnr` from HIP backend scaffolding (`float_ansnr_hip`)
which still has references in `docs/rebase-notes.md`.

---

## References

- PR #38: drop `float_ansnr` from C backend
- PR #86: CI infra agent T-LEGACY-RUNNER-ANSNR-BROKEN
- ADR-0749: formal sunset decision
- `docs/research/0733-feature-importance-audit-2026-05-28.md`
Loading