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 /
|
||||
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
|
||||
``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:
|
||||
* nonfinite objective values
|
||||
* invalid bounds / budget
|
||||
* sampled unimodality violation (values must decrease to the observed
|
||||
minimum then increase when sorted by coordinate)
|
||||
* sampled unimodality violation — reason ``sampled_unimodality_violation_observed``
|
||||
(values must decrease to the observed minimum then increase when sorted by
|
||||
coordinate)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -303,9 +312,14 @@ def fibonacci_section_search(
|
|||
k += 1
|
||||
|
||||
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(
|
||||
objective,
|
||||
reason="unimodality_violation_multiple_extrema_detected",
|
||||
reason="sampled_unimodality_violation_observed",
|
||||
a=a,
|
||||
b=b,
|
||||
evaluations=len(points),
|
||||
|
|
@ -333,7 +347,16 @@ def propose_kappa_from_search(
|
|||
*,
|
||||
baseline: float = BASELINE_KAPPA,
|
||||
) -> 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.
|
||||
"""
|
||||
|
|
|
|||
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