Skip to content
Draft
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
10 changes: 8 additions & 2 deletions predicators/agent_sdk/synthesis_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ class SynthesisBackend(Protocol):
_types: Set[Type]
_initial_predicates: Set[Predicate]
_initial_options: Set[ParameterizedOption]
# Per-fit cache of best-achievable per-segment RMS (system ID).
# Per-fit cache of the explainability sweep's verdicts (system ID):
# best-achievable per-segment RMS, each segment's argmin params, and
# whether ANY candidate reproduced its cascade -- the last being what
# decides explainability under interval scoring, where an RMS bar
# cannot separate "the twin did not reproduce which dominoes fall"
# from "it reproduced them with the wrong timing".
_explainability_cache: Dict[Tuple, Tuple[List[float], List[Dict[str,
float]]]]
float]],
List[bool]]]

# ── State written by the tools ───────────────────────────────
# Per-skill samplers keyed by option name.
Expand Down
4 changes: 2 additions & 2 deletions predicators/approaches/agent_sim_learning_approach.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,8 +804,8 @@ def __init__(self,
# reuse the sweep instead of re-rolling it, which both saves
# rollouts and pins the verdict for identical inputs.
self._explainability_cache: Dict[Tuple, Tuple[List[float],
List[Dict[str,
float]]]] = {}
List[Dict[str, float]],
List[bool]]] = {}
# Whole-fit memoization for the orchestrator (same lifecycle as
# the explainability cache): repeated canonical sim.fit calls on
# an unchanged artifact version + data reuse the entire fit core
Expand Down
47 changes: 27 additions & 20 deletions predicators/code_sim_learning/grid_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
scalar_from_fit_space, scalar_to_fit_space
from predicators.code_sim_learning.rollout_env import RolloutTrajectory
from predicators.code_sim_learning.rollout_objective import \
compute_rollout_sse, per_trajectory_rms
compute_rollout_sse, per_trajectory_scores
from predicators.code_sim_learning.trajectory_prep import ResidualScaling

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -351,17 +351,17 @@ def min_explainable_rms(
and unexplainable as the agent re-declared inits across calls
(observed on run_20260711_141026, cycle 2).
"""
best, _ = min_explainable_fits(base_env,
trajectories,
physical_specs,
residual_features,
rules=rules,
rule_specs=rule_specs,
latent_init=latent_init,
extra_candidates=extra_candidates,
scaling=scaling,
anchors=anchors,
config=config)
best, _, _ = min_explainable_fits(base_env,
trajectories,
physical_specs,
residual_features,
rules=rules,
rule_specs=rule_specs,
latent_init=latent_init,
extra_candidates=extra_candidates,
scaling=scaling,
anchors=anchors,
config=config)
return best


Expand All @@ -377,7 +377,7 @@ def min_explainable_fits(
scaling: Optional[ResidualScaling] = None,
anchors: Optional[Dict[str, float]] = None,
config: Optional[SysIdConfig] = None,
) -> Tuple[List[float], List[Dict[str, float]]]:
) -> Tuple[List[float], List[Dict[str, float]], List[bool]]:
""":func:`min_explainable_rms` plus each trajectory's argmin params.

The second return is, per trajectory, the PHYSICAL portion of the
Expand Down Expand Up @@ -412,12 +412,19 @@ def min_explainable_fits(
n: float(base[n])
for n in physical_names
} for _ in trajectories]
# Whether ANY candidate reproduced the cascade -- not whether the
# RMS-best one did. The two differ: under interval scoring a theta that
# completes the chain but mistimes it can score worse than one that
# stalls it early with few terms, and "could this model ever produce
# this outcome" is a question about the whole grid.
reproduced = [False] * len(trajectories)
for params in candidates:
rms = per_trajectory_rms(base_env, trajectories, params,
residual_features, physical_names, rules,
latent_init, scaling)
for i, r in enumerate(rms):
if r < best[i]:
best[i] = r
scores = per_trajectory_scores(base_env, trajectories, params,
residual_features, physical_names,
rules, latent_init, scaling)
for i, score in enumerate(scores):
reproduced[i] = reproduced[i] or score.stats.reproduced_cascade
if score.rms < best[i]:
best[i] = score.rms
best_params[i] = {n: float(params[n]) for n in physical_names}
return best, best_params
return best, best_params, reproduced
8 changes: 4 additions & 4 deletions predicators/code_sim_learning/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ def run_rollout_sysid(
rule_specs: Sequence[ParamSpec] = (),
latent_init: Any = None,
anchors: Optional[Dict[str, float]] = None,
rms_cache: Optional[Dict[Tuple, Tuple[List[float],
List[Dict[str, float]]]]] = None,
rms_cache: Optional[Dict[Tuple, Tuple[List[float], List[Dict[str, float]],
List[bool]]]] = None,
fit_cache: Optional[Dict[Tuple, _FitComputation]] = None,
fit_cache_key: Optional[Any] = None,
report_adjuster: Optional[ReportAdjuster] = None,
Expand Down Expand Up @@ -239,8 +239,8 @@ def _compute_fit(
rule_specs: Sequence[ParamSpec],
latent_init: Any,
anchors: Dict[str, float],
rms_cache: Optional[Dict[Tuple, Tuple[List[float], List[Dict[str,
float]]]]],
rms_cache: Optional[Dict[Tuple, Tuple[List[float], List[Dict[str, float]],
List[bool]]]],
config: SysIdConfig,
) -> _FitComputation:
"""The cacheable fit core: trim + fit + report on the survivors."""
Expand Down
61 changes: 48 additions & 13 deletions predicators/code_sim_learning/physical_sysid.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,8 +623,8 @@ def fit_params_rollout_trimmed(
noise_sigma: float = 0.05,
scaling: Optional[ResidualScaling] = None,
anchors: Optional[Dict[str, float]] = None,
rms_cache: Optional[Dict[Tuple, Tuple[List[float],
List[Dict[str, float]]]]] = None,
rms_cache: Optional[Dict[Tuple, Tuple[List[float], List[Dict[str, float]],
List[bool]]]] = None,
config: Optional[SysIdConfig] = None,
) -> Tuple[FitResult, List[RolloutTrajectory], List[float], List[Dict[str,
float]]]:
Expand Down Expand Up @@ -695,7 +695,8 @@ def fit_params_rollout_trimmed(
config=config)
return result, list(trajectories), [], []
cache_key: Optional[Tuple] = None
cached: Optional[Tuple[List[float], List[Dict[str, float]]]] = None
cached: Optional[Tuple[List[float], List[Dict[str, float]],
List[bool]]] = None
if rms_cache is not None:
cache_key = _explainability_cache_key(physical_specs, rule_specs,
trajectories, anchors, scaling,
Expand Down Expand Up @@ -725,17 +726,51 @@ def fit_params_rollout_trimmed(
"%.1fs.",
num_rollouts_run() - sweep_n0,
time.monotonic() - sweep_t0)
rms, argmins = cached
rms, argmins, reproduced = cached
threshold = factor * noise_sigma
survivors = [t for t, r in zip(trajectories, rms) if r <= threshold]
surv_argmins = [a for a, r in zip(argmins, rms) if r <= threshold]
# UNDER INTERVAL SCORING THE QUESTION IS STRUCTURAL, NOT NUMERIC. An RMS
# bar asks "how closely does the twin match", but the residuals it is
# applied to are a missing-cascade penalty (the twin did not reproduce
# WHICH dominoes fall) and a timing difference (both fell, at different
# moments) added together -- and those differ by three orders of
# magnitude, so the bar is really a penalty detector with a
# wildly-miscalibrated tail. On run_20260820_141450 a theta reproducing
# all four falls with ZERO penalties scored 1.21 against the 0.1 bar and
# was dropped as "unexplainable at any candidate params", while the same
# theta was reported by the sweep as explaining the data 1084x better
# than baseline.
#
# So ask the question that was meant: did any candidate reproduce the
# cascade? The leftover timing error is precisely what the fit exists to
# reduce, and using it to refuse to fit is circular. The per-step
# objective keeps the RMS bar, where residuals really are a
# dimensionless fraction of typical motion and the bar means what it says.
structural = config.score_observed_only

def _explainable(index: int) -> bool:
"""Whether trajectory ``index`` is worth fitting on."""
if structural:
return reproduced[index]
return rms[index] <= threshold

keep = [i for i in range(len(trajectories)) if _explainable(i)]
survivors = [trajectories[i] for i in keep]
surv_argmins = [argmins[i] for i in keep]
if len(survivors) < len(trajectories):
logger.info(
"Rollout sysID trimming: per-trajectory best RMS %s vs "
"threshold %.4f (%g x noise %.3f) — dropping %d of %d "
"unexplainable trajectories.", [f"{r:.4g}" for r in rms],
threshold, factor, noise_sigma,
len(trajectories) - len(survivors), len(trajectories))
if structural:
logger.info(
"Rollout sysID trimming: no candidate reproduced the cascade "
"for %d of %d trajectories (best RMS %s, kept for the fit "
"rather than compared against a bar) — dropping them.",
len(trajectories) - len(survivors), len(trajectories),
[f"{r:.4g}" for r in rms])
else:
logger.info(
"Rollout sysID trimming: per-trajectory best RMS %s vs "
"threshold %.4f (%g x noise %.3f) — dropping %d of %d "
"unexplainable trajectories.", [f"{r:.4g}" for r in rms],
threshold, factor, noise_sigma,
len(trajectories) - len(survivors), len(trajectories))
if not survivors:
logger.warning(
"Rollout sysID trimming: NO trajectory is explainable at any "
Expand All @@ -755,7 +790,7 @@ def fit_params_rollout_trimmed(
# cleanest data rather than the loudest.
consistency = config.consistency_factor
physical_names = [s.name for s in physical_specs]
best = [r for r in rms if r <= threshold]
best = [rms[i] for i in keep]
hull_candidates: List[Dict[str, float]] = []
while True:
result = fit_params_rollout(base_env,
Expand Down
Loading
Loading