Bellman Stopping Controller for Self-Refinement / bellman_stopping_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1"""Bellman stopping controller MVP and mechanism checks.
  2
  3Run with: /home/maxwelhelp/main/bin/python3 bellman_stopping_experiment.py
  4"""
  5import json
  6import math
  7import random
  8from dataclasses import dataclass
  9from pathlib import Path
 10import numpy as np
 11
 12SEED = 2092
 13rng = np.random.default_rng(SEED)
 14
 15@dataclass
 16class ToyEnv:
 17    n: int = 21                 # score states 0,.05,...,1
 18    max_steps: int = 5
 19
 20    def p_improve(self, s):
 21        # Refinement has diminishing returns at high verifier score.
 22        return 0.10 + 0.75 * (1.0 - s)
 23
 24    def transition(self, i):
 25        s = i / (self.n - 1)
 26        p = self.p_improve(s)
 27        return [(i, 1-p), (min(i+1, self.n-1), p)]
 28
 29    def payoff(self, i):
 30        # Normalized terminal task utility / calibrated pass probability.
 31        return i / (self.n - 1)
 32
 33
 34def bellman(env, cost):
 35    """Exact finite-horizon DP. V[k,i] is value with k refinements left."""
 36    H, n = env.max_steps, env.n
 37    V = np.zeros((H + 1, n), dtype=float)
 38    policy = np.zeros((H, n), dtype=bool)  # True = continue
 39    V[0] = np.arange(n) / (n - 1)
 40    q_all = np.zeros_like(policy, dtype=float)
 41    for left in range(1, H + 1):
 42        for i in range(n):
 43            expected = sum(prob * V[left-1][j] for j, prob in env.transition(i))
 44            q = -cost + expected
 45            q_all[left-1, i] = q
 46            policy[left-1, i] = q > env.payoff(i) + 1e-12
 47            V[left, i] = max(env.payoff(i), q)
 48    return V, policy, q_all
 49
 50
 51def simulate(env, cost, adaptive=True, fixed_steps=None, trials=100000):
 52    V, policy, q = bellman(env, cost)
 53    n_calls, qualities, objectives, stops = [], [], [], []
 54    for _ in range(trials):
 55        i = 0
 56        calls = 0
 57        while calls < env.max_steps:
 58            if adaptive:
 59                # policy indexed by remaining refinements.
 60                if not policy[env.max_steps - calls - 1, i]:
 61                    break
 62            elif calls >= fixed_steps:
 63                break
 64            s = i / (env.n - 1)
 65            if rng.random() < env.p_improve(s):
 66                i = min(i + 1, env.n - 1)
 67            calls += 1
 68        quality = env.payoff(i)
 69        n_calls.append(calls)
 70        qualities.append(quality)
 71        objectives.append(quality - cost * calls)
 72        stops.append(i)
 73    return {
 74        "calls": float(np.mean(n_calls)),
 75        "quality": float(np.mean(qualities)),
 76        "objective": float(np.mean(objectives)),
 77        "pass_at_0.75": float(np.mean(np.asarray(qualities) >= .75)),
 78        "stop_state_mean": float(np.mean(stops)),
 79    }
 80
 81
 82def threshold(policy_row, env):
 83    # Return first score at which policy stops; score monotonicity can be checked.
 84    stop = np.flatnonzero(~policy_row)
 85    return float(stop[0] / (env.n - 1)) if len(stop) else None
 86
 87
 88def main():
 89    env = ToyEnv()
 90    # Prediction 1: continuation at the initial state decreases monotonically with c.
 91    costs = [0.00, 0.02, 0.03, 0.035, 0.038, 0.040, 0.045, 0.10]
 92    initial_continue = []
 93    initial_thresholds = []
 94    boundary_monotone = []
 95    for c in costs:
 96        V, pol, q = bellman(env, c)
 97        initial_continue.append(bool(pol[env.max_steps-1, 0]))
 98        # At the first decision, policy row has max remaining horizon.
 99        row = pol[env.max_steps-1]
100        initial_thresholds.append(threshold(row, env))
101        # Stop region should be an upper score interval.
102        stop_indices = np.flatnonzero(~row)
103        boundary_monotone.append(bool(len(stop_indices) == 0 or np.all(np.diff(stop_indices) == 1)))
104
105    # Prediction 2: the stopping boundary moves to lower score as cost rises.
106    # Ignore c=0's no-stop case when calculating finite boundaries.
107    finite_thresholds = [x for x in initial_thresholds if x is not None]
108    threshold_nonincreasing = all(a >= b for a, b in zip(finite_thresholds, finite_thresholds[1:]))
109
110    # Prediction 3: Bellman value is non-increasing in cost and equals max(stop, continue).
111    bellman_residuals, value_by_cost = [], []
112    for c in costs:
113        V, pol, q = bellman(env, c)
114        residual = np.max(np.abs(V[env.max_steps] - np.maximum(env.payoff(np.arange(env.n)), q[env.max_steps-1])))
115        bellman_residuals.append(float(residual))
116        value_by_cost.append(float(V[env.max_steps, 0]))
117    value_nonincreasing = all(a >= b - 1e-12 for a, b in zip(value_by_cost, value_by_cost[1:]))
118
119    # Monte Carlo comparison at moderate cost, with identical starting state.
120    c_eval = 0.038
121    adaptive = simulate(env, c_eval, adaptive=True)
122    fixed = {str(k): simulate(env, c_eval, adaptive=False, fixed_steps=k) for k in [1, 3, 5]}
123
124    # A direct numerical check of Delta > c versus policy on every state/horizon.
125    V, pol, q = bellman(env, c_eval)
126    decisions_agree = bool(np.all(pol == (q > np.tile(env.payoff(np.arange(env.n)), (env.max_steps, 1)))))
127
128    report = {
129        "seed": SEED,
130        "environment": {"score_states": env.n, "max_refinements": env.max_steps,
131                         "transition": "score increases one grid step with p=0.10+0.75*(1-score)",
132                         "payoff": "score"},
133        "math_check": {
134            "max_bellman_residual": max(bellman_residuals),
135            "delta_gt_cost_matches_policy": decisions_agree,
136            "value_at_initial_state_by_cost": dict(zip(map(str, costs), value_by_cost)),
137        },
138        "predictions": {
139            "P1_initial_continue_decreases_with_cost": {
140                "predicted": "monotone non-increasing",
141                "costs": costs,
142                "observed_continue": initial_continue,
143                "observed_threshold_score": initial_thresholds,
144                "confirmed": bool(all(a >= b for a, b in zip([int(x) for x in initial_continue], [int(x) for x in initial_continue][1:])))
145            },
146            "P2_stopping_boundary_is_upper_score_region_and_moves_lower_with_cost": {
147                "predicted": "stop for sufficiently high score; boundary non-increasing in cost",
148                "boundary_monotone_each_cost": boundary_monotone,
149                "finite_boundaries": finite_thresholds,
150                "confirmed": bool(all(boundary_monotone) and threshold_nonincreasing),
151            },
152            "P3_bellman_value_decreases_with_cost": {
153                "predicted": "non-increasing value and exact max recursion",
154                "values": value_by_cost,
155                "residuals": bellman_residuals,
156                "confirmed": bool(value_nonincreasing and max(bellman_residuals) < 1e-12),
157            },
158        },
159        "simulation_cost_0.038": {"adaptive": adaptive, "fixed": fixed},
160        "simulation_cost_sweep": {
161            str(c): {"adaptive": simulate(env, c, adaptive=True, trials=30000)}
162            for c in costs
163        },
164    }
165    report["worked"] = all(x["confirmed"] for x in report["predictions"].values()) and decisions_agree
166    Path("results.json").write_text(json.dumps(report, indent=2))
167    print(json.dumps(report, indent=2))
168
169if __name__ == "__main__":
170    main()