Joint multi-measurement fitting, optimizer resume, and a runnable example - #40
Merged
Merged
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fits one parameter set against several measured curves recorded at different grazing angles, each keeping its own energy grid. Simulated efficiencies are reassembled by CaseExecutionResult.index because the parallel batch runner yields in completion order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Persists the Ax client snapshot alongside optimizer run state and an append-only trial log, so an interrupted run can continue and total_trials can be raised to extend a finished one. A problem fingerprint refuses to resume into a changed search space. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The best-fit plot now reuses the winning trial's simulated curve, and plots are rewritten only when the best improves. Penalized trials log the failing case or exception instead of failing silently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers out-of-order result reassembly, joint loss reductions, resuming with an extended total_trials, and the fingerprint and corruption guards. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AngleMeasurementSpec.from_mapping used a truthiness check that raised on numpy arrays, which callers naturally pass for prepared energy grids and efficiencies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A non-resumed run opened trial_records.jsonl in append mode, so records from an earlier run at the same output directory accumulated and a later resume double-counted them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A non-resumed run rewrote created on every checkpoint write, so it tracked the last write instead of when the run began. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brings in the Nevière differential-method solver series (PR #39). The CHANGELOG conflict was both sides adding bullets under Unreleased; both sets are kept. The merge auto-resolved cleanly everywhere else, but that was misleading: develop renamed the BatchSimulationRunner settings and added solver=/ solver_options= to the single-angle runner construction in objective.py, while this branch had added a second runner construction for the joint path that git left untouched with the old default_* kwargs. That call now matches the single-angle one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Joint fitting was hardcoded to "same grating, several grazing angles". Each measurement now carries its own conditions -- angle_mode, grazing_angle_deg, cff, diffraction_order, polarization -- inheriting whichever it does not set from the run, so one fit can span a whole measurement campaign. Numerical settings (fourier_orders, solver, backend, max_workers) stay run-level: they describe how a curve is computed, not what was measured. AngleMeasurementSpec becomes MeasurementSpec and JointAngleMeasurement becomes JointMeasurement, since neither is angle-specific any more. Neither name has shipped, so there is nothing to deprecate. polarization was absent from grax_opt entirely, so every optimizer fit had been silently running the run_simulation default of s -- the same gap develop just closed for the theta-search workflow. It is now a real argument on both configs. solver/solver_options reach the joint path and are recorded in best_result.json, matching the single-measurement optimizer. They join the resume fingerprint alongside each measurement's resolved conditions, so a resumed run cannot silently switch the physics it is fitting. Two fixes found while reviewing the branch: An interrupted run over-ran its trial budget on resume. The trial-record log is appended every trial but the Ax snapshot is only rewritten every checkpoint_interval trials, so an interruption left the log ahead of the snapshot; the recovered history then counted trials Ax was about to generate again, duplicating a row in trial_history.csv and running past total_trials. Records at or beyond the reconciled cursor are discarded now, since Ax is authoritative for what was issued. Only reachable with checkpoint_interval > 1, which is why the default of 1 hid it. The single-measurement evaluation allocated its efficiency array with np.empty and filled by result index, so a case the runner never returned left uninitialized memory as that point's efficiency. The joint path already guarded against this; both do now. examples/optimizer/optimizer_joint/ is the first runnable joint-fit workflow. It simulates a four-condition measurement set from a known grating, fits it, resumes to extend the budget, and reports how close each parameter came to the truth. The measurements are always generated with rcwa, so fitting them with --solver neviere recovers the same geometry to within a few parts in ten thousand. The example deliberately stops at 20 trials. The joint loss reaches the noise floor there; a 100-trial run was measured to improve it by 5.5% while making parameter recovery slightly worse, because past the floor the optimizer fits the noise realization rather than the grating. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Merges
develop(the Nevière differential-method series) into this branch, generalizes joint fitting beyond grazing angle, and adds the first runnable joint-fit example.Integrating develop
One textual conflict (
CHANGELOG.md, both sides adding bullets — both kept). Everything else auto-merged, but that was misleading: develop renamed theBatchSimulationRunnersettings and addedsolver=/solver_options=to the single-measurement runner construction, while this branch had added a second runner construction for the joint path that git left untouched with the olddefault_*kwargs. That would have been aTypeErroron the first joint trial.Generalizing the joint fit
Joint fitting was hardcoded to "same grating, several grazing angles". Each measurement now carries its own conditions —
angle_mode,grazing_angle_deg,cff,diffraction_order,polarization— inheriting whichever it does not set from the run. Numerical settings (fourier_orders,solver,backend,max_workers) stay run-level: they describe how a curve is computed, not what was measured.AngleMeasurementSpec→MeasurementSpecandJointAngleMeasurement→JointMeasurement. Neither name has shipped, so nothing to deprecate.polarizationwas absent fromgrax_optentirely, so every optimizer fit had been silently running therun_simulationdefault ofs— the same gap develop just closed for the theta-search workflow.solver/solver_optionsreach the joint path and are recorded inbest_result.json. They join the resume fingerprint alongside each measurement's resolved conditions, so a resumed run cannot silently switch the physics it is fitting.Two fixes found while reviewing the branch
Interrupted runs over-ran their trial budget on resume. The trial-record log is appended every trial but the Ax snapshot is only rewritten every
checkpoint_intervaltrials, so an interruption left the log ahead of the snapshot. The recovered history then counted trials Ax was about to generate again — duplicating a row intrial_history.csvand running pasttotal_trials. Reproduced before fixing: interrupting after 4 trials withcheckpoint_interval=3and a budget of 6 gave 7 completed trials and trial index 3 recorded twice. Records at or beyond the reconciled cursor are now discarded, since Ax is authoritative for what was issued. Only reachable withcheckpoint_interval > 1, which is why the default of1hid it.The single-measurement evaluation could read uninitialized memory. It allocated its efficiency array with
np.emptyand filled by result index, so a case the runner never returned left whatever was in memory as that point's efficiency. The joint path already guarded against this; both do now.The example
examples/optimizer/optimizer_joint/simulates a four-condition measurement set from a known grating, fits it, resumes to extend the budget, and reports how close each parameter came to the truth. The four conditions cover one generalization axis each: grazing angle, diffraction order, angle mode, polarization.The measurements are always generated with
rcwa, so fitting them with--solver neviererecovers the same geometry — width0.6428vs0.6422, depth14.6140vs14.6143.It deliberately stops at 20 trials. The joint loss reaches the noise floor there; a 100-trial run was measured and improved the loss by 5.5% while making parameter recovery slightly worse, because past the floor the optimizer fits the noise realization rather than the grating. The example reports which parameters came back tightly and which did not, computed from the data rather than asserted.
Verification
axandtorchblocked (mimicking CI without theoptextra): 89 passed, 1 skipped — the new unit tests are CI-safe.🤖 Generated with Claude Code