feat(adr-0242): Golden-Angle atlas packing + Fibonacci section search
Integrate Gemini ADR-0242 implementation with adversarial hardening: CGA null-point separation pin (d_min=0.12), fail-closed Fibonacci search with fixed budget and unimodality checks, serve quarantine still held, ADR-0242 Proposed (not pre-accepted), fidelity ledger flip to green.
This commit is contained in:
parent
0489b6a98a
commit
3086e9a4d9
7 changed files with 529 additions and 11 deletions
108
core/physics/atlas_packing.py
Normal file
108
core/physics/atlas_packing.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""core.physics.atlas_packing — Golden-Angle mode packing (ADR-0242).
|
||||
|
||||
Construction-boundary lift of Poincaré polar coordinates into Cl(4,1) null
|
||||
points via :func:`algebra.cga.embed_point`. Runtime storage is pure 32-vectors
|
||||
only (no Poincaré attribute leaks).
|
||||
|
||||
Separation uses the CGA null-point distance recovered from ``cga_inner``:
|
||||
|
||||
⟨P,Q⟩ = −d²/2 ⇒ d = √(−2⟨P,Q⟩)
|
||||
|
||||
which is the Euclidean distance of the embedded R³ points (see ``cga_inner``
|
||||
doc). This is the cohesion-plan ``d_min`` pin progressive form — not a full
|
||||
H² geodesic solver. Fail-closed if any pair has ``d < min_d``.
|
||||
|
||||
Off-serve: do not import from ``chat/runtime.py`` (A-04 quarantine).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from algebra.cga import cga_inner, embed_point
|
||||
from core.physics.wave_manifold import WaveManifold
|
||||
|
||||
PHI = (1.0 + math.sqrt(5.0)) / 2.0
|
||||
DEFAULT_MIN_D = 0.12
|
||||
|
||||
|
||||
class AtlasPackingError(ValueError):
|
||||
"""Fail-closed packing refusal (separation / bounds)."""
|
||||
|
||||
|
||||
def null_point_separation(p: np.ndarray, q: np.ndarray) -> float:
|
||||
"""CGA null-point separation d = √(max(0, −2⟨P,Q⟩))."""
|
||||
inner = float(cga_inner(np.asarray(p, dtype=np.float64), np.asarray(q, dtype=np.float64)))
|
||||
# Clamp tiny positive float dust from null-cone numerics.
|
||||
return math.sqrt(max(0.0, -2.0 * min(0.0, inner)))
|
||||
|
||||
|
||||
def golden_angle_pack(
|
||||
n: int,
|
||||
alpha: float,
|
||||
*,
|
||||
min_d: float = DEFAULT_MIN_D,
|
||||
) -> list[np.ndarray]:
|
||||
"""Golden-Angle packing on the Cl(4,1) null cone (horosphere lift).
|
||||
|
||||
For k = 0..n-1:
|
||||
θ_k = 2π k / φ
|
||||
r_k = tanh(α √k)
|
||||
(x,y) = (r cos θ, r sin θ) → embed_point → null 32-vector
|
||||
|
||||
Rejects with :class:`AtlasPackingError` if any pairwise separation < min_d.
|
||||
"""
|
||||
if n < 1:
|
||||
raise AtlasPackingError("n must be >= 1")
|
||||
if not math.isfinite(alpha) or alpha <= 0.0:
|
||||
raise AtlasPackingError("alpha must be a positive finite float")
|
||||
if not math.isfinite(min_d) or min_d < 0.0:
|
||||
raise AtlasPackingError("min_d must be a non-negative finite float")
|
||||
|
||||
modes: list[np.ndarray] = []
|
||||
for k in range(n):
|
||||
# 2π/φ ≈ 222.5°; packing-equivalent complement is the classic ~137.5° golden angle.
|
||||
theta_k = 2.0 * math.pi * k / PHI
|
||||
r_k = math.tanh(alpha * math.sqrt(float(k)))
|
||||
x = r_k * math.cos(theta_k)
|
||||
y = r_k * math.sin(theta_k)
|
||||
mode = embed_point(np.asarray([x, y, 0.0], dtype=np.float64), dtype=np.float64)
|
||||
modes.append(np.asarray(mode, dtype=np.float64))
|
||||
|
||||
for i in range(len(modes)):
|
||||
for j in range(i + 1, len(modes)):
|
||||
d = null_point_separation(modes[i], modes[j])
|
||||
if d < min_d:
|
||||
raise AtlasPackingError(
|
||||
f"Packing rejected: separation {d:.4f} between {i} and {j} "
|
||||
f"is less than required minimum {min_d:.4f}."
|
||||
)
|
||||
return modes
|
||||
|
||||
|
||||
def register_packed_modes(
|
||||
modes: Sequence[np.ndarray],
|
||||
manifold: WaveManifold,
|
||||
) -> tuple[int, ...]:
|
||||
"""Register packed null modes on a session WaveManifold. Returns indices.
|
||||
|
||||
Note: these are null-point modes for spectral span / packing geometry, not
|
||||
unit-versor holographic seals (seal_mode would refuse non-closed versors).
|
||||
"""
|
||||
indices: list[int] = []
|
||||
for mode in modes:
|
||||
indices.append(manifold.register_resonant_mode(mode))
|
||||
return tuple(indices)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PHI",
|
||||
"DEFAULT_MIN_D",
|
||||
"AtlasPackingError",
|
||||
"null_point_separation",
|
||||
"golden_angle_pack",
|
||||
"register_packed_modes",
|
||||
]
|
||||
174
core/physics/fibonacci_search.py
Normal file
174
core/physics/fibonacci_search.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""core.physics.fibonacci_search — fixed-budget Fibonacci section search (ADR-0242).
|
||||
|
||||
Deterministic 1D unimodal minimization for construction / calibration /
|
||||
GoldTether κ-style scalar brackets. Not a serve-path operator (A-04 quarantine).
|
||||
|
||||
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)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BoundedUnimodalObjective:
|
||||
lower: float
|
||||
upper: float
|
||||
evaluation_budget: int
|
||||
objective_id: str
|
||||
objective_version: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.evaluation_budget < 2:
|
||||
raise ValueError("evaluation_budget must be >= 2")
|
||||
if self.upper <= self.lower:
|
||||
raise ValueError("upper bound must be strictly greater than lower bound")
|
||||
if not math.isfinite(self.lower) or not math.isfinite(self.upper):
|
||||
raise ValueError("bounds must be finite")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SearchTrace:
|
||||
best_observed_point: float
|
||||
eval_sequence: list[float] = field(default_factory=list)
|
||||
certificate: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
def _fibonacci(n: int) -> int:
|
||||
"""F_0=0, F_1=1, … standard Fibonacci. n may be 0."""
|
||||
if n < 0:
|
||||
raise ValueError("fibonacci index must be non-negative")
|
||||
a, b = 0, 1
|
||||
for _ in range(n):
|
||||
a, b = b, a + b
|
||||
return a
|
||||
|
||||
|
||||
def _assert_sampled_unimodality(eval_values: dict[float, float]) -> None:
|
||||
"""Fail-closed if sorted samples are not unimodal about the observed min."""
|
||||
sorted_points = sorted(eval_values.keys())
|
||||
min_idx = 0
|
||||
min_val = float("inf")
|
||||
for i, x in enumerate(sorted_points):
|
||||
v = eval_values[x]
|
||||
if v < min_val:
|
||||
min_val = v
|
||||
min_idx = i
|
||||
|
||||
# Strictly non-increasing toward min (allow float ties).
|
||||
for i in range(min_idx):
|
||||
left = eval_values[sorted_points[i]]
|
||||
right = eval_values[sorted_points[i + 1]]
|
||||
if left < right - 1e-9:
|
||||
raise ValueError(
|
||||
"unimodality violation detected (multiple extrema): "
|
||||
"values not decreasing before minimum."
|
||||
)
|
||||
|
||||
# Strictly non-decreasing after min (allow float ties).
|
||||
for i in range(min_idx, len(sorted_points) - 1):
|
||||
left = eval_values[sorted_points[i]]
|
||||
right = eval_values[sorted_points[i + 1]]
|
||||
if left > right + 1e-9:
|
||||
raise ValueError(
|
||||
"unimodality violation detected (multiple extrema): "
|
||||
"values not increasing after minimum."
|
||||
)
|
||||
|
||||
|
||||
def fibonacci_section_search(
|
||||
objective: BoundedUnimodalObjective,
|
||||
func: Callable[[float], float],
|
||||
) -> SearchTrace:
|
||||
"""Fibonacci section search: exactly ``evaluation_budget`` function evals.
|
||||
|
||||
Returns :class:`SearchTrace` with ``best_observed_point``, ``eval_sequence``,
|
||||
and a small certificate dict (budget, ids, bounds).
|
||||
"""
|
||||
n = int(objective.evaluation_budget)
|
||||
a = float(objective.lower)
|
||||
b = float(objective.upper)
|
||||
|
||||
f_n_plus_1 = _fibonacci(n + 1)
|
||||
f_n_minus_1 = _fibonacci(n - 1)
|
||||
f_n = _fibonacci(n)
|
||||
|
||||
c = a + (f_n_minus_1 / f_n_plus_1) * (b - a)
|
||||
d = a + (f_n / f_n_plus_1) * (b - a)
|
||||
|
||||
def _eval(x: float) -> float:
|
||||
if x < objective.lower - 1e-12 or x > objective.upper + 1e-12:
|
||||
raise ValueError(f"bounds violation: evaluated {x} outside [{objective.lower}, {objective.upper}]")
|
||||
y = float(func(x))
|
||||
if not math.isfinite(y):
|
||||
raise ValueError(f"Objective function returned nonfinite value {y} at {x}")
|
||||
return y
|
||||
|
||||
fc = _eval(c)
|
||||
fd = _eval(d)
|
||||
eval_sequence = [c, d]
|
||||
eval_values: dict[float, float] = {c: fc, d: fd}
|
||||
|
||||
best_x = c if fc < fd else d
|
||||
best_f = min(fc, fd)
|
||||
|
||||
k = 1
|
||||
while k < n - 1:
|
||||
if fc < fd:
|
||||
b = d
|
||||
d = c
|
||||
fd = fc
|
||||
f_n_minus_k_minus_1 = _fibonacci(n - k - 1)
|
||||
f_n_minus_k_plus_1 = _fibonacci(n - k + 1)
|
||||
c = a + (f_n_minus_k_minus_1 / f_n_minus_k_plus_1) * (b - a)
|
||||
fc = _eval(c)
|
||||
eval_sequence.append(c)
|
||||
eval_values[c] = fc
|
||||
if fc < best_f:
|
||||
best_f = fc
|
||||
best_x = c
|
||||
else:
|
||||
a = c
|
||||
c = d
|
||||
fc = fd
|
||||
f_n_minus_k = _fibonacci(n - k)
|
||||
f_n_minus_k_plus_1 = _fibonacci(n - k + 1)
|
||||
d = a + (f_n_minus_k / f_n_minus_k_plus_1) * (b - a)
|
||||
fd = _eval(d)
|
||||
eval_sequence.append(d)
|
||||
eval_values[d] = fd
|
||||
if fd < best_f:
|
||||
best_f = fd
|
||||
best_x = d
|
||||
k += 1
|
||||
|
||||
_assert_sampled_unimodality(eval_values)
|
||||
|
||||
certificate = {
|
||||
"budget": objective.evaluation_budget,
|
||||
"objective_id": objective.objective_id,
|
||||
"objective_version": objective.objective_version,
|
||||
"lower_bound": objective.lower,
|
||||
"upper_bound": objective.upper,
|
||||
"best_value": best_f,
|
||||
"n_evals": len(eval_sequence),
|
||||
}
|
||||
return SearchTrace(
|
||||
best_observed_point=float(best_x),
|
||||
eval_sequence=list(eval_sequence),
|
||||
certificate=certificate,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BoundedUnimodalObjective",
|
||||
"SearchTrace",
|
||||
"fibonacci_section_search",
|
||||
]
|
||||
75
docs/adr/ADR-0242-atlas-packing-and-fibonacci.md
Normal file
75
docs/adr/ADR-0242-atlas-packing-and-fibonacci.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# ADR-0242: Hyperbolic Atlas Golden-Angle Packing and Fibonacci Search
|
||||
|
||||
**Status**: Proposed — packing + Fibonacci search green on branch; acceptance path: Joshua review + merge
|
||||
**Date**: 2026-07-14
|
||||
**Deciders**: Joshua Shay + multi-model R&D (Gemini implementation pass)
|
||||
**Traceability**: PR #37, parent ADR-0241 / cohesion master plan
|
||||
**Related**: ADR-0003, ADR-0238, ADR-0241, `docs/analysis/core_cohesion_master_plan.md`, `docs/briefs/ADR-0242-atlas-packing-and-fibonacci-brief.md`
|
||||
**Canonical path**: `docs/adr/`
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0241 established `WaveManifold` and `HolographicVaultStore`. Entity cohesion still needed:
|
||||
|
||||
1. **Uniform resonant-mode packing** without resurrecting pointwise `core_ha` node IDs or Poincaré as runtime memory truth (ADR-0003).
|
||||
2. **Fixed-budget unimodal scalar search** for construction/calibration (e.g. GoldTether κ brackets) without scipy-as-truth or stochastic optimizers.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Golden-Angle packing (`core/physics/atlas_packing.py`)
|
||||
|
||||
For \(k = 0 \ldots n-1\):
|
||||
|
||||
\[
|
||||
\theta_k = 2\pi k / \varphi,\qquad r_k = \tanh(\alpha\sqrt{k})
|
||||
\]
|
||||
|
||||
Lift \((r\cos\theta, r\sin\theta, 0)\) via `algebra.cga.embed_point` to Cl(4,1) **null points**.
|
||||
|
||||
**Separation pin:** CGA null-point distance from `cga_inner` contract \(\langle P,Q\rangle = -d^2/2\):
|
||||
|
||||
\[
|
||||
d = \sqrt{-2\langle P,Q\rangle}
|
||||
\]
|
||||
|
||||
Fail-closed (`AtlasPackingError`) if any pair has \(d < d_{\min}\) (default \(0.12\)).
|
||||
|
||||
**Honest scope:** this \(d\) is the Euclidean distance of the embedded \(\mathbb{R}^3\) points (null-cone isometric readout), not a full hyperbolic \(H^2\) geodesic solver. Sufficient for the cohesion packing density gate.
|
||||
|
||||
**No attribute leaks:** returned modes are pure `float64` 32-vectors. No stored θ/r.
|
||||
|
||||
**Not holographic seals:** packed null points are session mode-registry geometry; `HolographicVaultStore.seal_mode` still requires closed unit versors.
|
||||
|
||||
### 2. Fibonacci section search (`core/physics/fibonacci_search.py`)
|
||||
|
||||
- `BoundedUnimodalObjective(lower, upper, evaluation_budget, objective_id, objective_version)`
|
||||
- `fibonacci_section_search(objective, func) -> SearchTrace`
|
||||
- Exactly `evaluation_budget` evaluations
|
||||
- Fail-closed on nonfinite, bounds violation, sampled unimodality violation
|
||||
- Certificate carries budget, ids, bounds, best value, n_evals
|
||||
|
||||
### 3. Serve quarantine (A-04)
|
||||
|
||||
Neither module may be imported from `chat/runtime.py`. Pinned in `tests/test_third_door_cohesion.py`.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
- Deterministic atlas packing for standing-wave mode placement
|
||||
- Algebra-native fixed-budget scalar search for κ / residual brackets
|
||||
- Continues `core_ha` deprecation (no node IDs / Poincaré runtime store)
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- Separation is CGA null-point Euclidean distance, not full hyperbolic geodesic
|
||||
- Unimodality check is sample-based (only evaluated points), not a global oracle
|
||||
- Packing modes are null points, not unit versors — durable vault seal path remains separate
|
||||
|
||||
## Validation
|
||||
|
||||
- `tests/test_adr_0242_atlas_packing.py`
|
||||
- `tests/test_adr_0242_fibonacci.py`
|
||||
- `tests/test_third_door_cohesion.py` (serve quarantine + κ integration)
|
||||
|
|
@ -299,8 +299,8 @@ PY
|
|||
| Serve path not wired to wave / Fibonacci (containment) | 🟢 (AST-pinned in cohesion suite) |
|
||||
| Entity I-01…I-05 cohesion suite | 🟢 progressive pins in `test_third_door_cohesion.py` (I-02 float32-honest) |
|
||||
| Vault public `get_versor` ABI | 🟢 |
|
||||
| Golden-Angle atlas packing \(d_{\min}=0.12\) | 🔴 STOP → `docs/briefs/ADR-0242-atlas-packing-and-fibonacci-brief.md` |
|
||||
| Fibonacci κ search | 🔴 STOP → same brief |
|
||||
| Golden-Angle atlas packing \(d_{\min}=0.12\) | 🟢 ADR-0242 (`atlas_packing`; CGA null-point \(d\)) |
|
||||
| Fibonacci κ search | 🟢 ADR-0242 (`fibonacci_search`) |
|
||||
|
||||
### Subsumption map (Slice 2–3)
|
||||
| Operator | Delegation |
|
||||
|
|
@ -337,6 +337,6 @@ PY
|
|||
| `core_ha` deprecation — 🟢 no live tree + hygiene + Phase 0 grep | ADR-0241 / deprecation plan |
|
||||
| Durable holographic vault spectrum — 🟢 HolographicVaultStore | ADR-0241 |
|
||||
| Entity cohesion I-01…I-05 + Trace A/B | cohesion master plan |
|
||||
| Atlas packing + Fibonacci κ (ADR-0242) | cohesion master plan |
|
||||
| Atlas packing + Fibonacci κ (ADR-0242) — 🟢 packing + search | PR #37 / ADR-0242 |
|
||||
|
||||
Closing a gap = flip its `xfail` in `tests/test_third_door_blueprint_fidelity.py` (or the ADR-0241 / cohesion suite) to a passing behavioral test and delete the matching characterization lock. That is the definition of "done right" here.
|
||||
|
|
|
|||
73
tests/test_adr_0242_atlas_packing.py
Normal file
73
tests/test_adr_0242_atlas_packing.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
"""ADR-0242 — Golden-Angle atlas packing behavioral pins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from algebra.cga import is_null
|
||||
from core.physics.atlas_packing import (
|
||||
DEFAULT_MIN_D,
|
||||
AtlasPackingError,
|
||||
golden_angle_pack,
|
||||
null_point_separation,
|
||||
register_packed_modes,
|
||||
)
|
||||
from core.physics.wave_manifold import WaveManifold
|
||||
|
||||
|
||||
def test_golden_angle_pack_n_modes_min_geodesic_ge_0_12():
|
||||
modes = golden_angle_pack(n=10, alpha=0.5)
|
||||
assert len(modes) == 10
|
||||
min_d = min(
|
||||
null_point_separation(modes[i], modes[j])
|
||||
for i in range(len(modes))
|
||||
for j in range(i + 1, len(modes))
|
||||
)
|
||||
assert min_d >= DEFAULT_MIN_D
|
||||
|
||||
|
||||
def test_golden_angle_pack_rejects_when_alpha_too_dense():
|
||||
with pytest.raises(AtlasPackingError, match="separation"):
|
||||
golden_angle_pack(n=50, alpha=0.01)
|
||||
|
||||
|
||||
def test_packing_lift_produces_closed_or_null_legal_points():
|
||||
modes = golden_angle_pack(n=5, alpha=0.3)
|
||||
for m in modes:
|
||||
assert is_null(m), "Lifted points must be legal null points in CGA"
|
||||
assert m.shape == (32,)
|
||||
assert m.dtype == np.float64
|
||||
|
||||
|
||||
def test_packing_deterministic_for_fixed_alpha_n():
|
||||
modes1 = golden_angle_pack(n=20, alpha=0.4)
|
||||
modes2 = golden_angle_pack(n=20, alpha=0.4)
|
||||
for m1, m2 in zip(modes1, modes2):
|
||||
np.testing.assert_allclose(m1, m2)
|
||||
|
||||
|
||||
def test_no_poincare_runtime_storage_in_wave_or_vault_metadata_truth():
|
||||
manifold = WaveManifold()
|
||||
modes = golden_angle_pack(n=5, alpha=0.3)
|
||||
idxs = register_packed_modes(modes, manifold)
|
||||
assert len(idxs) == 5
|
||||
for m in manifold.resonant_modes:
|
||||
assert m.shape == (32,)
|
||||
assert m.dtype == np.float64
|
||||
assert not hasattr(m, "theta")
|
||||
assert not hasattr(m, "r")
|
||||
# Modes are plain arrays — no Poincaré sidecar attributes.
|
||||
assert not hasattr(modes[0], "theta")
|
||||
|
||||
|
||||
def test_null_point_separation_matches_euclidean_embed():
|
||||
"""cga_inner contract: ⟨P,Q⟩ = −d²/2 for embedded Euclidean points."""
|
||||
from algebra.cga import embed_point
|
||||
|
||||
p = embed_point(np.asarray([0.1, 0.0, 0.0], dtype=np.float64), dtype=np.float64)
|
||||
q = embed_point(np.asarray([0.4, 0.0, 0.0], dtype=np.float64), dtype=np.float64)
|
||||
d = null_point_separation(p, q)
|
||||
assert abs(d - 0.3) < 1e-9
|
||||
77
tests/test_adr_0242_fibonacci.py
Normal file
77
tests/test_adr_0242_fibonacci.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
"""ADR-0242 — Fibonacci section search behavioral pins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from core.physics.fibonacci_search import (
|
||||
BoundedUnimodalObjective,
|
||||
fibonacci_section_search,
|
||||
)
|
||||
|
||||
|
||||
def test_fibonacci_search_hits_known_unimodal_min_within_1e_3():
|
||||
objective = BoundedUnimodalObjective(
|
||||
lower=0.1,
|
||||
upper=2.0,
|
||||
evaluation_budget=20,
|
||||
objective_id="test_id",
|
||||
objective_version="v1",
|
||||
)
|
||||
|
||||
def func(x: float) -> float:
|
||||
return (x - 0.789) ** 2
|
||||
|
||||
trace = fibonacci_section_search(objective, func)
|
||||
assert abs(trace.best_observed_point - 0.789) < 1e-3
|
||||
assert len(trace.eval_sequence) == 20
|
||||
|
||||
|
||||
def test_fibonacci_search_eval_count_equals_budget():
|
||||
objective = BoundedUnimodalObjective(
|
||||
lower=-5.0,
|
||||
upper=5.0,
|
||||
evaluation_budget=15,
|
||||
objective_id="test_id2",
|
||||
objective_version="v1",
|
||||
)
|
||||
|
||||
def func(x: float) -> float:
|
||||
return x**2
|
||||
|
||||
trace = fibonacci_section_search(objective, func)
|
||||
assert len(trace.eval_sequence) == 15
|
||||
assert trace.certificate["n_evals"] == 15
|
||||
|
||||
|
||||
def test_fibonacci_search_rejects_nan_objective():
|
||||
objective = BoundedUnimodalObjective(
|
||||
lower=-1.0,
|
||||
upper=1.0,
|
||||
evaluation_budget=10,
|
||||
objective_id="nan",
|
||||
objective_version="v1",
|
||||
)
|
||||
|
||||
def func(x: float) -> float:
|
||||
return float("nan")
|
||||
|
||||
with pytest.raises(ValueError, match="nonfinite"):
|
||||
fibonacci_section_search(objective, func)
|
||||
|
||||
|
||||
def test_fibonacci_search_unimodality_violation_fail_closed():
|
||||
objective = BoundedUnimodalObjective(
|
||||
lower=-2.0,
|
||||
upper=2.0,
|
||||
evaluation_budget=10,
|
||||
objective_id="multi",
|
||||
objective_version="v1",
|
||||
)
|
||||
|
||||
def func(x: float) -> float:
|
||||
# Multiple extrema: x^4 - x^2
|
||||
return x**4 - x**2
|
||||
|
||||
with pytest.raises(ValueError, match="unimodality"):
|
||||
fibonacci_section_search(objective, func)
|
||||
|
|
@ -269,11 +269,22 @@ def test_resonant_reconstruct_empty_refused():
|
|||
# --- ADR-0242 placeholder (Fibonacci not yet landed) --------------------------
|
||||
|
||||
|
||||
def test_fibonacci_search_module_absent_or_importable_placeholder():
|
||||
"""Until P5, fibonacci_search may be absent; if present it must not hit serve."""
|
||||
spec = importlib.util.find_spec("core.physics.fibonacci_search")
|
||||
if spec is None:
|
||||
pytest.skip("ADR-0242 fibonacci_search not landed yet (expected until P5)")
|
||||
# If present, ensure runtime still quarantined (A-04 already covers imports).
|
||||
mod = importlib.import_module("core.physics.fibonacci_search")
|
||||
assert hasattr(mod, "fibonacci_section_search")
|
||||
def test_fibonacci_search_goldtether_integration():
|
||||
"""Asserts Fibonacci search can optimize kappa and return a valid certificate."""
|
||||
from core.physics.fibonacci_search import BoundedUnimodalObjective, fibonacci_section_search
|
||||
|
||||
objective = BoundedUnimodalObjective(
|
||||
lower=0.1,
|
||||
upper=2.0,
|
||||
evaluation_budget=20,
|
||||
objective_id="sha256_mock_id_for_goldtether_kappa",
|
||||
objective_version="v1.0",
|
||||
)
|
||||
|
||||
def synthetic_objective(kappa: float) -> float:
|
||||
return (kappa - 0.789) ** 2 # unimodal minimum at 0.789
|
||||
|
||||
trace = fibonacci_section_search(objective, synthetic_objective)
|
||||
assert abs(trace.best_observed_point - 0.789) < 1e-3
|
||||
assert len(trace.eval_sequence) == 20
|
||||
assert trace.certificate.get("budget") == 20
|
||||
|
|
|
|||
Loading…
Reference in a new issue