SysID: PHYSICAL_PARAMS system identification via free-running rollout fit#48
Merged
Conversation
This was referenced Jul 7, 2026
Teacher-forced single-step fitting cannot see physical parameters of momentum-driven dynamics (states carry pose but no velocity), so friction/restitution were invisible to the existing objective. New code_sim_learning/physical_sysid.py fits agent-declared PHYSICAL_PARAMS by matching free-running base-sim rollouts of full trajectories, jointly with rule PARAM_SPECS in one posterior, and reports per-parameter identifiability (posterior contraction) instead of regularizing it away. - BaseEnv.get_physical_param_info / apply_physical_param_overrides: envs advertise tunable physics and accept sticky in-place overrides; implemented for pybullet_domino (friction, restitution, mass, rolling/spinning friction). - Synthesis prompt gains an optional PHYSICAL_PARAMS section built from the env's revealed menu; evaluate_step_fit switches to the joint rollout fit when the artifact declares PHYSICAL_PARAMS and appends the identifiability report. - Physics-only artifacts (no PROCESS_RULES) are now valid; identified values are applied to the planning base env so refinement and test planning use calibrated physics. - _solve_lm grows a diff_step override: simulation residuals need a coarser finite-difference step or the Jacobian is identically zero. - New CFG code_sim_learning_rollout_num_mcmc_steps keeps the costly rollout-fit MCMC budget separate (default 0 = LM point fit).
resetBaseVelocity alone leaves articulated joint velocities behind, and _set_state's per-component diff skips joints whose positions already match, so consecutive rollouts on the shared fit env inherited up to ~1.8 rad/s of residual arm momentum. Position control overrides it within a step or two, so this is hygiene rather than the (engine internal) source of the observed same-theta SSE jitter, but rollouts genuinely must start at rest.
The prior-scale curvature probe measured the SSE increase against a single MAP evaluation, so on chaotic data the same-theta evaluation jitter itself read as curvature and every parameter was declared identified (run_20260705_203314: ~5k d_sse inside a ~8k jitter, contraction 0.00 on all params, false confidence in friction 0.5). Evaluate the MAP SSE three times, take the median, and subtract the observed spread from every perturbation response before it counts as curvature; a flat-after-discount direction reports NOT identified.
Two measured blockers on real domino data (true friction 0.1, planning 0.5): - A free-running rollout diverges chaotically from the recording over hundreds of contact steps, and the long settled tail re-scores that accumulated divergence every step: on full 500-step trajectories the SSE at the TRUE friction exceeded the SSE at the wrong one. Cutting each trajectory after its last observed scored-feature motion (plus a settle margin) restores the signal: a clean top-edge push goes from ratio 0.9 to ~5e4 wrong/true SSE. - The SSE landscape is flat above friction ~0.5 (topple reach saturates), so LM's finite differences see no gradient from the declared init and the fit stalls even on clean data. A coarse per-parameter grid sweep relocates the LM starting point into the best-scoring basin; the Gaussian prior stays centered on the declared init. Together these recover friction 0.07-0.08 from a single clean recorded push (init 0.5); previously the fit returned the init unchanged.
…ation The fit consumed whatever exploration recorded, and applied whatever it fitted. Both trusted too much (run_20260706_111805: a chaotic scraping push dragged the pooled friction fit to 0.34, and rolling/spinning friction were applied at arbitrary values their own identifiability report called NOT identified). Three layers, each verified against real recorded episodes: - Explainability trimming (min_explainable_rms): judge each trajectory by its best-achievable RMS over a candidate param grid — judged against its own best params, not the pooled fit, which chaos poisons — and drop recordings no parameters can explain before fitting. - Consistency loop: at the joint fit every survivor must fit nearly as well as its own best; on disagreement (a recording can be accidentally explainable at WRONG params, e.g. a quiet shove at friction ~1.34 vs the clean topple's 0.1) drop the survivor with the largest best-RMS and refit, anchoring on the cleanest data. - select_trustworthy_params: apply a fitted physical value to the planning env only when its identifiability verdict is at least weakly identified; keep the declared init otherwise. If nothing survives trimming, the result is pinned at the declared inits so a false-positive verdict on chaotic data cannot leak arbitrary values. Callers compute post-SSE and the identifiability probe on the SURVIVING trajectories only. E2E on real recordings: chaotic-only data keeps the declared inits with honest NOT-identified verdicts; chaotic pooled with one clean topple trims the chaos and recovers friction 0.0706 (true 0.1) plus rolling_friction 0.0078 (true 0.006).
run_20260706_171526 fit friction 0.0114 for a true 0.1: the linear grid over [0.01, 2.0] has no candidate between 0.01 and 0.29, the sweep jumped to the 0.01 endpoint, LM saw no gradient across the flat low-friction basin, and the linear curvature probe blessed the bound-hugging MAP as identified. The believed sim ended up 9x too slippery and drop-place plans verified in sim but failed in the real env (the 2/5 final-test failures). ParamSpec gains scale="log" for positive scale-like parameters. The optimizer runs in z = log(theta): geometric grid sweeps (a candidate lands at ~0.098), relative LM finite-difference steps, a log-normal prior (x2 up and x2 down equally plausible), log-space identifiability contraction, and fit-space Laplace/perturbation ensembles. Everything simulator- and caller-facing stays in linear units; FitResult samples stay external while jacobian/prior_sigma live in fit space (scales records the mapping). The domino env registry marks friction, spinning_friction, and mass as log-scale (restitution and rolling_friction keep lo=0, stay linear), and the approach/tool paths stamp the registry scale onto the agent-declared PHYSICAL_PARAMS so agents need not know about it. E2E-verified on a same-engine cascade at true friction 0.1 from init 0.5: log-scale recovers 0.0987 (SSE 0.705 -> 0.0005); the old linear path reproduces the 0.0100 endpoint failure on identical data.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Commits
PHYSICAL_PARAMSsystem identification via free-running rollout fit - newcode_sim_learning/physical_sysid.pyfit stack, wired through the agent SDK tools,agent_sim_learning_approach, and per-envPHYSICAL_PARAMSdeclarations onbase_env/ domino.z = log(theta)fit stack (geomspace grid, log-normal prior, log probe, fit-space ensembles); recovers friction ~0.0987 vs a linear fit's 0.0100 on identical data.Contains one hand-resolved
settings.pyconflict (keptnum_mcmc_stepshere; the default-to-0 change lands in the configs PR).