refactor(adr-0244): D3 search honesty — sampled-unimodality naming + kappa legibility
Cohesion directive Mandate 6 (ADR-0244 §2.4). The Fibonacci search was already ~85% compliant (typed OptimizationFailure, fail-closed, bracketed contract, never a bare float); this closes the two residuals honestly: - Rename the failure reason unimodality_violation_multiple_extrema_detected -> sampled_unimodality_violation_observed. A finite sample cannot prove global unimodality; the check only *observes* a violation on the evaluated points. Docstrings now state the Bracketed-Local contract explicitly. - propose_kappa_from_search: kappa=1.0-on-failure IS the required "parameters unchanged" no-op (thr = productive_threshold / 1.0), and the typed OptimizationFailure is already returned as the second element — so the fix is legibility, not a behavior change (the working seam is preserved, not broken): the docstring names baseline as an explicit caller-side policy, and a new test pins that a failed search surfaces the typed failure rather than a bare float. tests: test_adr_0244_search_honesty pins the renamed reason end-to-end (a cos(8*pi*x) objective whose golden-section samples are non-monotone triggers it; a smooth bimodal instead converges to a local-min certificate — the honest Bracketed-Local distinction), _unimodality_ok as a finite-sample check, kappa failure legibility + no-op baseline, and that no stale reason string remains. [Verification]: search-honesty 6 + adr_0242_fibonacci + third_door_cohesion + carry_seams green; smoke + fast lane below.
This commit is contained in:
parent
82b031d158
commit
3a0cd0b455
2 changed files with 119 additions and 5 deletions
|
|
@ -3,14 +3,23 @@
|
||||||
Deterministic 1D unimodal minimization for construction / calibration /
|
Deterministic 1D unimodal minimization for construction / calibration /
|
||||||
GoldTether κ-style scalar brackets. Not a serve-path operator (A-04 quarantine).
|
GoldTether κ-style scalar brackets. Not a serve-path operator (A-04 quarantine).
|
||||||
|
|
||||||
|
A strictly **Bracketed Local** refinement operator (ADR-0244 §2.4): the caller
|
||||||
|
must supply a pre-bracketed interval around a known minimum. Unimodality cannot
|
||||||
|
be proven from a finite sample, so the check is honestly named a *sampled*
|
||||||
|
unimodality violation — an observation on the evaluated points, not a global
|
||||||
|
guarantee on the unsampled portions of ``[a, b]``.
|
||||||
|
|
||||||
Public result is always a typed ``FibonacciSearchCertificate`` or
|
Public result is always a typed ``FibonacciSearchCertificate`` or
|
||||||
``OptimizationFailure`` — never a bare float (Drive evidence discipline).
|
``OptimizationFailure`` — never a bare float (Drive evidence discipline). On any
|
||||||
|
fail-closed condition the operator returns a typed failure; it never silently
|
||||||
|
substitutes a fallback parameter in-path (that policy belongs to the caller).
|
||||||
|
|
||||||
Fail-closed on:
|
Fail-closed on:
|
||||||
* nonfinite objective values
|
* nonfinite objective values
|
||||||
* invalid bounds / budget
|
* invalid bounds / budget
|
||||||
* sampled unimodality violation (values must decrease to the observed
|
* sampled unimodality violation — reason ``sampled_unimodality_violation_observed``
|
||||||
minimum then increase when sorted by coordinate)
|
(values must decrease to the observed minimum then increase when sorted by
|
||||||
|
coordinate)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -303,9 +312,14 @@ def fibonacci_section_search(
|
||||||
k += 1
|
k += 1
|
||||||
|
|
||||||
if not _unimodality_ok(eval_values):
|
if not _unimodality_ok(eval_values):
|
||||||
|
# A finite sample cannot prove global unimodality on the unsampled
|
||||||
|
# portions of [a, b]; it can only *observe* a violation on the sampled
|
||||||
|
# points. Honest name (ADR-0244 §2.4 / directive M6): the operator is a
|
||||||
|
# Bracketed Local refiner and fails closed here rather than silently
|
||||||
|
# defaulting parameters in-path.
|
||||||
return _failure(
|
return _failure(
|
||||||
objective,
|
objective,
|
||||||
reason="unimodality_violation_multiple_extrema_detected",
|
reason="sampled_unimodality_violation_observed",
|
||||||
a=a,
|
a=a,
|
||||||
b=b,
|
b=b,
|
||||||
evaluations=len(points),
|
evaluations=len(points),
|
||||||
|
|
@ -333,7 +347,16 @@ def propose_kappa_from_search(
|
||||||
*,
|
*,
|
||||||
baseline: float = BASELINE_KAPPA,
|
baseline: float = BASELINE_KAPPA,
|
||||||
) -> tuple[float, SearchResult]:
|
) -> tuple[float, SearchResult]:
|
||||||
"""Evidence-gated κ: cert → minimizer; failure → baseline (default 1.0).
|
"""Evidence-gated κ from a search result. Proposal-only telemetry.
|
||||||
|
|
||||||
|
On a certificate, returns the certified minimizer. On an
|
||||||
|
``OptimizationFailure``, returns ``baseline`` — which is an **explicit
|
||||||
|
caller-side policy**, not a search-internal default: κ = 1.0 is the identity
|
||||||
|
no-op (``thr = productive_threshold / 1.0`` leaves the active threshold
|
||||||
|
unchanged), so a failed search never moves live parameters (ADR-0244 §2.4 /
|
||||||
|
directive M6). The typed ``OptimizationFailure`` is always returned as the
|
||||||
|
second element, so a caller can distinguish "search proposed κ = 1.0" from
|
||||||
|
"search failed → holding the κ = 1.0 no-op" instead of reading a bare float.
|
||||||
|
|
||||||
Never promotes COHERENT standing or mutates identity — caller telemetry only.
|
Never promotes COHERENT standing or mutates identity — caller telemetry only.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
91
tests/test_adr_0244_search_honesty.py
Normal file
91
tests/test_adr_0244_search_honesty.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""ADR-0244 §2.4 search-honesty pins (cohesion directive Mandate 6).
|
||||||
|
|
||||||
|
The Fibonacci section search is a Bracketed Local refiner: it observes a
|
||||||
|
*sampled* unimodality violation (finite samples cannot prove global
|
||||||
|
unimodality), fails closed with the honestly-named reason, and never silently
|
||||||
|
substitutes a fallback parameter in-path. ``propose_kappa_from_search`` keeps
|
||||||
|
the κ = 1.0 no-op on failure (the unchanged-threshold policy) but returns the
|
||||||
|
typed failure so a caller can tell "proposed 1.0" from "failed → holding 1.0".
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.physics.fibonacci_search import (
|
||||||
|
BASELINE_KAPPA,
|
||||||
|
BoundedUnimodalObjective,
|
||||||
|
FibonacciSearchCertificate,
|
||||||
|
OptimizationFailure,
|
||||||
|
_unimodality_ok,
|
||||||
|
fibonacci_section_search,
|
||||||
|
propose_kappa_from_search,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _obj(lo: float, hi: float, budget: int = 16) -> BoundedUnimodalObjective:
|
||||||
|
return BoundedUnimodalObjective(
|
||||||
|
lower=lo,
|
||||||
|
upper=hi,
|
||||||
|
evaluation_budget=budget,
|
||||||
|
objective_id="search_honesty_test",
|
||||||
|
objective_version="v1",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _multimodal(x: float) -> float:
|
||||||
|
# Highly oscillatory → the golden-section samples straddle several extrema,
|
||||||
|
# so the *sampled* trajectory is non-monotone and the violation fires. (A
|
||||||
|
# smooth bimodal, by contrast, converges to one local min — the search is a
|
||||||
|
# Bracketed-Local refiner and reports a certificate for that min, not a
|
||||||
|
# violation. The violation is an honest-sample observation, not a global
|
||||||
|
# unimodality proof.)
|
||||||
|
return math.cos(8.0 * math.pi * x)
|
||||||
|
|
||||||
|
|
||||||
|
def _single_well(x: float) -> float:
|
||||||
|
return (x - 0.3) ** 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_sampled_unimodality_check_is_a_finite_sample_observation() -> None:
|
||||||
|
# Monotone down-then-up over the sampled points → OK.
|
||||||
|
assert _unimodality_ok({0.0: 3.0, 1.0: 1.0, 2.0: 2.0}) is True
|
||||||
|
# A bump before the observed minimum → not monotone-to-min → violation.
|
||||||
|
assert _unimodality_ok({0.0: 1.0, 1.0: 0.5, 2.0: 0.8, 3.0: 0.3}) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_multimodal_objective_returns_sampled_unimodality_violation() -> None:
|
||||||
|
result = fibonacci_section_search(_obj(0.0, 1.0), _multimodal)
|
||||||
|
assert isinstance(result, OptimizationFailure)
|
||||||
|
assert result.reason == "sampled_unimodality_violation_observed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unimodal_objective_returns_certificate() -> None:
|
||||||
|
result = fibonacci_section_search(_obj(-2.0, 2.0), _single_well)
|
||||||
|
assert isinstance(result, FibonacciSearchCertificate)
|
||||||
|
assert abs(result.minimizer - 0.3) < 0.2
|
||||||
|
|
||||||
|
|
||||||
|
def test_kappa_failure_is_legible_and_holds_the_no_op_baseline() -> None:
|
||||||
|
result = fibonacci_section_search(_obj(0.0, 1.0), _multimodal)
|
||||||
|
kappa, outcome = propose_kappa_from_search(result)
|
||||||
|
# baseline κ = 1.0 is the unchanged-threshold no-op...
|
||||||
|
assert kappa == BASELINE_KAPPA == 1.0
|
||||||
|
# ...and the failure is legibly the second element, not swallowed.
|
||||||
|
assert isinstance(outcome, OptimizationFailure)
|
||||||
|
assert outcome.reason == "sampled_unimodality_violation_observed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_kappa_success_returns_certified_minimizer() -> None:
|
||||||
|
result = fibonacci_section_search(_obj(-2.0, 2.0), _single_well)
|
||||||
|
kappa, outcome = propose_kappa_from_search(result)
|
||||||
|
assert isinstance(outcome, FibonacciSearchCertificate)
|
||||||
|
assert abs(kappa - 0.3) < 0.2
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_stale_reason_string_in_source() -> None:
|
||||||
|
src = Path(__file__).resolve().parents[1] / "core" / "physics" / "fibonacci_search.py"
|
||||||
|
text = src.read_text(encoding="utf-8")
|
||||||
|
assert "sampled_unimodality_violation_observed" in text
|
||||||
|
assert "unimodality_violation_multiple_extrema_detected" not in text
|
||||||
Loading…
Reference in a new issue