Third warning made visible by #206 scoping optimize()'s silencing to the call itself. Notebook 4's sweep prints this repeatedly:
pynumdiff/polynomial_fit.py:152: UserWarning: Kernel window size should be odd. Added 1 to length.
The warning is correct and the correction is correct. The problem is that optimize() handed the method a parameter set it had to fix up, and then returned that same uncorrected set to the caller.
How it happens
_objective_function converts a continuous point to parameters generically:
point_params= {k:(vifsearch_space_types[k] ==floatelseint(np.round(v)))
fork,vinzip(search_space_types, point)}Nothing stops that from producing window_size=10. savgoldiff then bumps it to 11 and warns. Measured over notebook 4's six simulations, optimize(savgoldiff, ...) returned an even window_size in 3 of 6 cases (cruise 10, triangle 10, lorenz 44).
Three consequences:
- The returned
best_params are not the parameters that actually ran. Re-calling the method with them re-triggers the correction and the warning. window_size=10 and window_size=11 are aliases for the same fit, but hash to different cache keys, so identical work gets done and stored twice.- Log noise proportional to the size of the sweep.
The dilemma
int(np.round(v)) is deliberately generic — it must serve every integer parameter, and only some of them are odd-constrained. Special-casing inside it would put per-parameter knowledge in a general mechanism.
But the per-parameter conversion rule already exists at optimize.py:213:
search_space_types= {k:type(v[0]) fork,vinsearch_space.items() ifisinstance(v, list)}That maps a parameter name to a type, and the consumer switches on it. Storing a converter instead — identity for float, round for int, round-to-nearest-odd for odd ints — keeps the cast fully generic while moving the odd-ness knowledge into the search space definition, next to bounds, where each method's parameter knowledge already lives.
Open question: how to express "odd int" in the existing spec vocabulary, where list/set/scalar already mean numerical/categorical/singleton and the type is inferred from type(v[0]). Probably a marker class or a parallel dict.
Scope limit
This only fully solves the per-parameter constraints. savgoldiff also clips window_size to [degree+1, x.shape[axis]-1] and polydiff raises it to degree*3+1 — both depend on other parameters, so no per-parameter rule can guarantee "returned parameters are always exactly what ran." Fully closing that would require methods reporting their canonicalization back to the optimizer, which is a much larger change than this warrants. The odd constraint is the one actually generating noise.
Note this changes results: restricting proposals to odd values alters the Nelder-Mead trajectory, so it shouldn't land mid-experiment.
🤖 Written by Claude Code on behalf of @pavelkomarov
Third warning made visible by #206 scoping
optimize()'s silencing to the call itself. Notebook 4's sweep prints this repeatedly:The warning is correct and the correction is correct. The problem is that
optimize()handed the method a parameter set it had to fix up, and then returned that same uncorrected set to the caller.How it happens
_objective_functionconverts a continuous point to parameters generically:Nothing stops that from producing
window_size=10.savgoldiffthen bumps it to 11 and warns. Measured over notebook 4's six simulations,optimize(savgoldiff, ...)returned an evenwindow_sizein 3 of 6 cases (cruise10,triangle10,lorenz44).Three consequences:
best_paramsare not the parameters that actually ran. Re-calling the method with them re-triggers the correction and the warning.window_size=10andwindow_size=11are aliases for the same fit, but hash to different cache keys, so identical work gets done and stored twice.The dilemma
int(np.round(v))is deliberately generic — it must serve every integer parameter, and only some of them are odd-constrained. Special-casing inside it would put per-parameter knowledge in a general mechanism.But the per-parameter conversion rule already exists at
optimize.py:213:That maps a parameter name to a type, and the consumer switches on it. Storing a converter instead — identity for float,
roundfor int, round-to-nearest-odd for odd ints — keeps the cast fully generic while moving the odd-ness knowledge into the search space definition, next tobounds, where each method's parameter knowledge already lives.Open question: how to express "odd int" in the existing spec vocabulary, where list/set/scalar already mean numerical/categorical/singleton and the type is inferred from
type(v[0]). Probably a marker class or a parallel dict.Scope limit
This only fully solves the per-parameter constraints.
savgoldiffalso clipswindow_sizeto[degree+1, x.shape[axis]-1]andpolydiffraises it todegree*3+1— both depend on other parameters, so no per-parameter rule can guarantee "returned parameters are always exactly what ran." Fully closing that would require methods reporting their canonicalization back to the optimizer, which is a much larger change than this warrants. The odd constraint is the one actually generating noise.Note this changes results: restricting proposals to odd values alters the Nelder-Mead trajectory, so it shouldn't land mid-experiment.
🤖 Written by Claude Code on behalf of @pavelkomarov