Every run dumps its resolved config to plot_dir/tab_config_<run_id>.yaml (_run_tabascal_impl.py:499). That file is the natural way to reproduce a run — and feeding it back in silently narrows the gain priors by two orders of magnitude.
validate_gain_scales converts both widths out of their public units and writes the result back into the same dict the runner later dumps (gains.py:132-148):
gains_config["amp_std"] = _positive_scale(
"amp_std", gp_amp_std,
lambda std: float(std) / 100 * gains_config["amp_mean"], # percent -> fraction
...
)
amp_std is documented and given as a percentage of amp_mean; phase_std in degrees. After validation the dict holds a fraction and radians. Nothing marks it as converted, so the second pass converts it again.
Measured on the shipped code with amp_std: 10, phase_std: 30:
pass 1 (the config as the runner then dumps it): amp_std = 0.1 phase_std = 0.523599
pass 2 (that dump, loaded and run again): amp_std = 0.001 phase_std = 0.00913852
-> amplitude prior narrowed 100x; phase 57x
A 10 % amplitude prior becomes 0.1 %, and a 30° phase prior becomes about half a degree. Both are still perfectly valid numbers, so nothing complains; the run just fits with a prior far tighter than the one written down, and a gain error the fit should have absorbed shows up in the residuals instead.
It bites anyone who does the obvious thing with the file the tool itself wrote — rerunning it, diffing two runs' configs, or lifting a block out of one into a new config.
Suggested shape
Keep config values in their public units and do the conversion where it is consumed: return the converted scale to the component rather than writing it back, or store it under a separate key the validator does not read. Whichever way, a config that has been through validation should still mean the same thing as the one the user wrote — the dumped file is a user-facing artefact.
Worth checking the other sections for the same pattern while fixing it; this one was found by looking, not by a sweep.
How this was found
A retrospective Codex pass over the config surface after the five config-changing PRs (#212, #216, #218, #225, #228), verified by hand against the shipped code.
Every run dumps its resolved config to
plot_dir/tab_config_<run_id>.yaml(_run_tabascal_impl.py:499). That file is the natural way to reproduce a run — and feeding it back in silently narrows the gain priors by two orders of magnitude.validate_gain_scalesconverts both widths out of their public units and writes the result back into the same dict the runner later dumps (gains.py:132-148):amp_stdis documented and given as a percentage ofamp_mean;phase_stdin degrees. After validation the dict holds a fraction and radians. Nothing marks it as converted, so the second pass converts it again.Measured on the shipped code with
amp_std: 10,phase_std: 30:A 10 % amplitude prior becomes 0.1 %, and a 30° phase prior becomes about half a degree. Both are still perfectly valid numbers, so nothing complains; the run just fits with a prior far tighter than the one written down, and a gain error the fit should have absorbed shows up in the residuals instead.
It bites anyone who does the obvious thing with the file the tool itself wrote — rerunning it, diffing two runs' configs, or lifting a block out of one into a new config.
Suggested shape
Keep config values in their public units and do the conversion where it is consumed: return the converted scale to the component rather than writing it back, or store it under a separate key the validator does not read. Whichever way, a config that has been through validation should still mean the same thing as the one the user wrote — the dumped file is a user-facing artefact.
Worth checking the other sections for the same pattern while fixing it; this one was found by looking, not by a sweep.
How this was found
A retrospective Codex pass over the config surface after the five config-changing PRs (#212, #216, #218, #225, #228), verified by hand against the shipped code.