Skip to content
Open
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
8 changes: 4 additions & 4 deletions predicators/agent_sdk/tools/synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,10 @@ def _evaluate_rollout_fit(rules: list,
"not a parameter value.")
else:
lines.append(
"Applied to the planning base env: the most likely value "
"of every parameter. sim.run, sim.refine, experiment "
"scores and execution monitoring draw the parameters "
"from this belief.")
"Applied to the planning base env: the fit's point "
"estimate of every parameter. sim.run, sim.refine, "
"experiment scores and execution monitoring draw the "
"parameters from this belief.")
return "\n".join(lines)
interval_belief = CFG.code_sim_learning_interval_belief
if interval_belief:
Expand Down
20 changes: 20 additions & 0 deletions predicators/code_sim_learning/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ class SysIdConfig:
# scales (predicators/observation_noise.py); None when observations
# are exact or the channel is undeclared.
observation_noise: Optional[ObservationNoise] = None
# The fit's starting values are the agent's guesses
# (code_sim_learning_prior_spans_bounds), not calibrated baselines.
anchors_are_guesses: bool = False

@property
def flat_band_frac(self) -> float:
"""The relative part of the flat band (``grid_flat_frac``), 0 when the
anchors are guesses.

The band lets a candidate within ``grid_flat_frac`` of the
model-bias SSE count as data-equivalent, so the anchor-nearest
one wins: a deference to the anchor beyond the prior, which a
calibrated baseline earns and a guess does not. With guesses,
data-equivalence is the likelihood floor alone
(:func:`~predicators.code_sim_learning.grid_seed.flat_tolerance`),
the scale the parameter belief scores with, so the fitted point
stays inside the belief's high-density region.
"""
return 0.0 if self.anchors_are_guesses else self.grid_flat_frac

@classmethod
def from_cfg(cls) -> SysIdConfig:
Expand Down Expand Up @@ -162,4 +181,5 @@ def from_cfg(cls) -> SysIdConfig:
track_frame_yaw=CFG.code_sim_learning_track_frame_yaw,
track_frame_xy=tuple(CFG.code_sim_learning_track_frame_xy),
observation_noise=_declared_observation_noise(),
anchors_are_guesses=CFG.code_sim_learning_prior_spans_bounds,
)
2 changes: 1 addition & 1 deletion predicators/code_sim_learning/grid_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def _grid_seed_physical_specs(
num_points = config.grid_seed_points
num_passes = max(1, config.grid_sweep_passes)
refine_evals = config.grid_refine_evals
flat_frac = config.grid_flat_frac
flat_frac = config.flat_band_frac
physical_names = [s.name for s in physical_specs]
all_specs = list(physical_specs) + list(rule_specs)
names = [s.name for s in all_specs]
Expand Down
70 changes: 66 additions & 4 deletions predicators/code_sim_learning/parameter_belief.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,27 @@ def quantile(self, q: float) -> float:
return float(self.grid[0])
return float(self._invert(np.array([q]))[0])

def mode(self) -> float:
"""The value of highest density."""
return float(self.grid[int(np.argmax(self.density))])

def highest_density_interval(self, coverage: float) -> Tuple[float, float]:
"""The span of the densest pieces that together hold at least
``coverage`` of the mass.

For a unimodal density this is the shortest interval with that
mass, and it always contains the mode; a central interval
excludes a mode that sits at a bound.
"""
if self.is_point:
return float(self.grid[0]), float(self.grid[0])
a, b, fa, fb, mass = self._pieces()
order = np.argsort(-(fa + fb), kind="stable")
cum = np.cumsum(mass[order])
count = int(np.searchsorted(cum, coverage * cum[-1])) + 1
chosen = order[:min(count, order.size)]
return float(np.min(a[chosen])), float(np.max(b[chosen]))


@dataclass(frozen=True)
class DiscretePosterior:
Expand Down Expand Up @@ -222,6 +243,10 @@ def quantile(self, q: float) -> float:
idx = int(np.searchsorted(cum, min(max(q, 0.0), 1.0) - 1e-12))
return float(self.values[min(idx, self.values.size - 1)])

def mode(self) -> float:
"""The most probable value."""
return float(self.values[int(np.argmax(self.probs))])


@dataclass
class ParameterBelief:
Expand Down Expand Up @@ -297,8 +322,41 @@ def interval(self,
return float(np.exp(ends[0])), float(np.exp(ends[1]))
return float(ends[0]), float(ends[1])

def most_likely(self, name: str) -> float:
"""The mode of ``name``'s factor (external units); the fit's point
estimate for a held parameter."""
if name in self.discrete:
return self.discrete[name].mode()
line = self.lines.get(name)
if line is None:
return float(self.map_estimate[name])
mode = line.mode()
if self.scales[self.names.index(name)] == "log":
return float(np.exp(mode))
return float(mode)

def highest_density_interval(self,
name: str,
coverage: float = 0.68
) -> Tuple[float, float]:
"""The highest-density ``coverage`` interval of ``name``'s factor
(external units), which contains its mode."""
line = self.lines.get(name)
if line is None or name in self.discrete:
return self.interval(name, coverage)
lo, hi = line.highest_density_interval(coverage)
if self.scales[self.names.index(name)] == "log":
return float(np.exp(lo)), float(np.exp(hi))
return float(lo), float(hi)

def describe(self) -> List[str]:
"""One readable line per parameter, plus the noise-level note."""
"""One readable line per parameter, plus the noise-level note.

Each line gives the factor's own most likely value and its 68%
highest-density interval; the fit's point estimate, which the
planning model runs at, is named when it lies outside that
interval.
"""
lines = []
if self.noise_scale > 1.0:
lines.append(
Expand All @@ -317,9 +375,13 @@ def describe(self) -> List[str]:
f"{dist.probs[i]:.2f}" for i in top)
lines.append(f" {name}: discrete, posterior {shares}")
continue
lo, hi = self.interval(name)
lines.append(f" {name}: most likely {value:.4g}; 68% "
f"posterior interval [{lo:.4g}, {hi:.4g}]")
lo, hi = self.highest_density_interval(name)
line = (f" {name}: most likely {self.most_likely(name):.4g}; 68% "
f"posterior interval [{lo:.4g}, {hi:.4g}]")
if not lo <= value <= hi:
line += (f"; the fit's point estimate {value:.4g} lies "
"outside it")
lines.append(line)
return lines

def to_dict(self) -> Dict[str, Any]:
Expand Down
8 changes: 6 additions & 2 deletions predicators/code_sim_learning/physical_sysid.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,11 @@ def fit_params_rollout(
sensitivity=sensitivity,
lm_notes=lm_notes)
n_lm = num_rollouts_run() - n_start - n_grid
if (config.anchor_ablation and config.grid_flat_frac > 0 and trajectories):
# The ablation pins moved parameters back to their anchors, which
# only calibrated baselines warrant; a guess has no claim beyond the
# prior the fit already folds in.
if (config.anchor_ablation and not config.anchors_are_guesses
and config.grid_flat_frac > 0 and trajectories):
result = _anchor_backward_elimination(
base_env,
trajectories,
Expand Down Expand Up @@ -538,7 +542,7 @@ def refit_pinned(surviving: List[ParamSpec],
# The grid flat set's tolerance (interval-belief terms included,
# see grid_seed.flat_tolerance), so "data-equivalent" means the
# same thing here as in the sweep.
tol = flat_tolerance(sse_curr, noise_floor, config.grid_flat_frac,
tol = flat_tolerance(sse_curr, noise_floor, config.flat_band_frac,
noise_sse, sigma_tol)
cost_curr = prior_cost(point)
best: Optional[Tuple[float, ParamSpec, Dict[str, float]]] = None
Expand Down
7 changes: 7 additions & 0 deletions predicators/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2410,6 +2410,13 @@ class GlobalSettings:
# wide as its range, or the belief claims a knowledge it lacks
# (2026-10-04: a spinning friction started at 0.05 came out
# 0.057-0.12 with the world at 0.5).
# Guesses earn no deference beyond that prior either: the grid's flat
# band drops its relative term (SysIdConfig.flat_band_frac), so
# data-equivalence is the likelihood floor the belief scores with,
# and the anchor ablation does not run. With the band, a spinning
# friction whose best candidate (0.67, world 0.5) beat the guessed
# 0.005 by 2.8 nats stayed at 0.005, outside the belief's own 68%
# interval of 0.39 to 0.71 (2026-10-09).
code_sim_learning_prior_spans_bounds = False
# Goodness-of-fit trimming threshold for rollout sysID, as a
# multiple of the fit's noise_sigma (0 disables): a segment whose
Expand Down
3 changes: 3 additions & 0 deletions tests/code_sim_learning/test_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def _trajectory(num_steps=10, gain=2.0):
def test_run_rollout_sysid_fit_cache_and_report_isolation():
"""The fit core is memoized per (artifact, data) key; adjusters and trust
selection stay caller-local on deep-copied reports."""
# The trust selection runs only without the joint belief, and an
# earlier test may leave belief_joint_draws set.
utils.reset_config({})
env = _GainEnv()
spec = ParamSpec("gain", 1.0, lo=0.1, hi=10.0, scale="log")
traj = _trajectory()
Expand Down
32 changes: 32 additions & 0 deletions tests/code_sim_learning/test_parameter_belief.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,38 @@ def test_line_posterior_moments_and_sampling():
assert np.all(point.sample(np.random.default_rng(0), 3) == 0.4)


def test_highest_density_interval_holds_a_mode_at_a_bound():
"""A density that falls away from a bound has its mode at the bound: the
central interval excludes it, the highest-density interval starts there and
holds at least the same mass."""
line = LinePosterior.from_neg_log([0.0, 1.0, 2.0], [0.0, 1.0, 2.0])
assert line.mode() == 0.0
assert line.quantile(0.16) > 0.0
lo, hi = line.highest_density_interval(0.68)
assert lo == 0.0
inside = (line.grid >= lo) & (line.grid <= hi)
mass = np.trapz(line.density[inside], line.grid[inside])
assert 0.68 <= mass <= 0.72
# A narrower interval with the same mass does not exist.
assert hi - lo <= line.quantile(0.84) - line.quantile(0.16)


def test_report_names_a_point_estimate_outside_the_belief():
"""The report gives the belief's own most likely value and 68% interval,
and names the fit's point estimate when it lies outside that interval."""
specs = [ParamSpec("a", 0.0, lo=-1.0, hi=1.0)]
residuals = _gaussian_residuals(["a"], [0.3], [[20.0]])
off = _build(specs, {"a": 0.0}, residuals)
lo, hi = off.highest_density_interval("a")
assert lo <= off.most_likely("a") <= hi
assert off.most_likely("a") == pytest.approx(0.3, abs=0.02)
assert not lo <= 0.0 <= hi
text = "\n".join(off.describe())
assert "the fit's point estimate 0 lies outside it" in text
on = _build(specs, {"a": 0.3}, residuals)
assert "point estimate" not in "\n".join(on.describe())


def test_gaussian_lines_recover_conditional_variances():
"""Each line has the mean-field variance ``1 / Lambda_jj``."""
specs = [
Expand Down
75 changes: 75 additions & 0 deletions tests/code_sim_learning/test_physical_sysid.py
Original file line number Diff line number Diff line change
Expand Up @@ -1434,6 +1434,81 @@ def _ablation_fixtures():
return spec_a, spec_b, anchors, prior_sigma, config, traj


def _spin_sse(params):
"""Spinning friction above 0.3 fits 2% better: inside the relative flat
band of the model-bias SSE, beyond a zero likelihood floor."""
return 10.0 - (0.2 if params["spinning_friction"] > 0.3 else 0.0)


def test_grid_sweep_defers_to_calibrated_anchors_only(monkeypatch):
"""A calibrated anchor keeps its value when a better candidate lies inside
the relative flat band; a guessed one does not.

With guessed starting values (code_sim_learning_prior_spans_bounds)
the flat band is the likelihood floor alone, the scale the parameter
belief scores with. Seed 0 of the Domino round fixes_r3 kept a
guessed spinning friction of 0.005 although its best candidate, 0.67
with the world at 0.5, was 2.8 nats better, outside the belief's own
68% interval.
"""
specs = [ParamSpec("spinning_friction", 0.005, lo=0.0, hi=1.0)]
anchors = {"spinning_friction": 0.005}
seeds, _info = _run_sweep(monkeypatch,
_spin_sse,
specs,
anchors,
code_sim_learning_prior_spans_bounds=False)
assert seeds["spinning_friction"] == 0.005
seeds, _info = _run_sweep(monkeypatch,
_spin_sse,
specs,
anchors,
code_sim_learning_prior_spans_bounds=True)
assert seeds["spinning_friction"] > 0.3


def test_anchor_ablation_runs_for_calibrated_anchors_only(monkeypatch):
"""The anchor ablation pins moved parameters back to calibrated baselines;
with guessed starting values it does not run, since a guess has no claim
beyond the prior the fit folds in."""
from predicators.settings import CFG
specs = [ParamSpec("mu", 0.5, lo=0.1, hi=1.0)]
calls = []

def fake_lm(_env,
_trajs,
physical_specs,
_features,
_rules=(),
rule_specs=(),
_latent=None,
**_kwargs):
all_specs = list(physical_specs) + list(rule_specs)
return np.array([s.init_value for s in all_specs], dtype=float), None

def fake_grid(_env, _trajs, physical_specs, *_args, **_kwargs):
return list(physical_specs), {}

def fake_ablation(*args, **_kwargs):
calls.append(True)
return args[11] # the fit result, unchanged

monkeypatch.setattr(physical_sysid, "fit_map_lm_rollout", fake_lm)
monkeypatch.setattr(physical_sysid, "_grid_seed_physical_specs", fake_grid)
monkeypatch.setattr(physical_sysid, "compute_rollout_sse",
lambda *_args, **_kwargs: 1.0)
monkeypatch.setattr(physical_sysid, "_anchor_backward_elimination",
fake_ablation)
trajectory = _trajectory([0.0, 0.1, 0.2])
for guesses, ran in ((False, True), (True, False)):
monkeypatch.setattr(CFG, "code_sim_learning_prior_spans_bounds",
guesses)
calls.clear()
physical_sysid.fit_params_rollout(None, [trajectory], specs,
_RESIDUAL_FEATURES)
assert bool(calls) is ran


def test_anchor_ablation_reverts_compensatory_param():
"""A co-adapted MAP on the compensation ridge is resolved to the anchor-
consistent basin: the tight-prior param reverts to its anchor and the wide-
Expand Down
Loading